برنامه نویس فان | Fun Developer یک آدم ساده که عاشق برنامه نویسی و کد زدنه :) تلاش میکنه تا به بقیه کمک کنه. توسعه دهنده هسته لاراول و فضای اوپن سورس. فاندر پرانتز و کد نیوز.

سالید (SOLID) یک واژهی اختصاری (mnemonic acronym) است که توسط Robert C. Martin (معروف به Uncle Bob) معرفی شده و بیانگر پنج اصل طراحی شیءگرا (Object-Oriented Design) است:
-
Single Responsibility Principle (SRP) — اصل تک مسئولیتی
-
Open/Closed Principle (OCP) — اصل باز/بسته
-
Liskov Substitution Principle (LSP) — اصل جایگزینی لیسکوف
-
Interface Segregation Principle (ISP) — اصل تفکیک واسط
-
Dependency Inversion Principle (DIP) — اصل وارونگی وابستگی
گرچه این اصول از دنیای برنامهنویسی شیءگرا منشأ گرفتهاند، اما بهطور گسترده در بسیاری از زبانها و الگوهای برنامهنویسی — از جمله زبان Go — قابل اجرا هستند.
Single Responsibility Principle (SRP)
تعریف: «یک کلاس (یا در Go، یک struct یا component) باید تنها یک و فقط یک دلیل برای تغییر داشته باشد.»
اصل تک مسئولیتپذیری (Single Responsibility Principle یا SRP) تأکید میکند که هر جزء از نرمافزار شما باید تنها مسئول یک بخش خاص از عملکرد باشد. تغییرات باید به یک جزء مرتبط با آن تغییر مربوط شوند.
در زبان گو، اغلب با structها یا پکیجهای بزرگی مواجه میشویم که بیشازحد کار انجام میدهند — از جمله مدیریت HTTP، کوئریهای دیتابیس، Business Logic و..، همگی در یک مکان هستند. این کار با اصل SRP در تضاد است و میتواند باعث ایجاد کدی شود که نگهداری یا تست آن دشوار است.
اصل SRP همچنین به این معناست که ماژولها، پکیجها یا structها باید هدفی مشخص و تعریفشده داشته باشند. اگر هنگام توصیف وظایف یک struct از واژه «و» استفاده میکنید (مثلاً «این struct هم فلان کار را انجام میدهد و هم بهمان کار را») شاید وقت آن رسیده که آن را به اجزای کوچکتر و متمرکزتر تقسیم کنید.
مثال بدون استفاده از SRP:
package main
import (
"fmt"
"os"
"time"
)
type UserService struct {
// Potentially various fields related to user operations
}
func (us *UserService) CreateUser(name string, email string) error {
// 1. Validate the user data
if name == "" || email == "" {
return fmt.Errorf("name and email cannot be empty")
}
// 2. Business logic: storing user in DB (simulated)
fmt.Printf("User %s with email %s created.
", name, email)
// 3. Logging to a file
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open log file: %v", err)
}
defer file.Close()
logEntry := fmt.Sprintf("%s: Created user %s with email %s
", time.Now().Format(time.RFC3339), name, email)
if _, err = file.WriteString(logEntry); err != nil {
return fmt.Errorf("failed to write to log file: %v", err)
}
return nil
}
func main() {
us := &UserService{}
err := us.CreateUser("Alice", "alice@example.com")
if err != nil {
fmt.Println("Error:", err)
}
}
مشکلات مثال بالا:
- سرویس
UserService
مسئول ایجاد کاربر و همچنین لاگگیری است. - اگر نیازمندیهای مربوط به لاگگیری تغییر کنند (مثلاً استفاده از یک سرویس لاگ ابری)، باید
UserService
را تغییر دهیم. - این باعث میشود کد سختتر قابل نگهداری و تست باشد — چون منطق تجاری (business logic) با لاگگیری درهم آمیخته شده و cohesion کد کاهش یافته است.
با SRP:
package main
import (
"fmt"
"os"
"time"
)
// Logger defines the behavior of a logger.
type Logger interface {
Log(message string) error
}
// FileLogger implements Logger for logging to a file.
type FileLogger struct {
filePath string
}
func (fl *FileLogger) Log(message string) error {
file, err := os.OpenFile(fl.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open log file: %v", err)
}
defer file.Close()
logEntry := fmt.Sprintf("%s: %s
", time.Now().Format(time.RFC3339), message)
if _, err := file.WriteString(logEntry); err != nil {
return fmt.Errorf("failed to write to log file: %v", err)
}
return nil
}
// UserService handles user-related logic.
type UserService struct {
logger Logger
}
func NewUserService(logger Logger) *UserService {
return &UserService{logger: logger}
}
func (us *UserService) CreateUser(name string, email string) error {
// Business logic only
if name == "" || email == "" {
return fmt.Errorf("name and email cannot be empty")
}
// Simulate user creation
fmt.Printf("User %s with email %s created.
", name, email)
// Delegating logging responsibility to the Logger
return us.logger.Log(fmt.Sprintf("Created user %s with email %s", name, email))
}
func main() {
fileLogger := &FileLogger{filePath: "app.log"}
userService := NewUserService(fileLogger)
if err := userService.CreateUser("Alice", "alice@example.com"); err != nil {
fmt.Println("Error:", err)
}
}
مزایا:
- اکنون
UserService
فقط مسئول ایجاد کاربر و منطق تجاری مرتبط با آن است. -
FileLogger
مسئولیت لاگگیری را بر عهده دارد. - این تفکیک مسئولیت باعث افزایش انسجام، سهولت نگهداری و تستپذیری کد میشود.
Open/Closed Principle (OCP)
تعریف: «ماژول های نرمافزاری باید برای گسترش باز و برای تغییر بسته باشند.»
اصل باز/بسته (Open/Closed Principle) بیان میکند که باید بتوانید قابلیتهای جدید را بدون تغییر کدهای موجود و تستشده اضافه کنید.
به جای تغییر مستقیم در کدهای قبلی، شما باید آنها را گسترش دهید — معمولاً با استفاده از interfaceها، composition یا سایر روشهای انتزاع (abstraction).
این رویکرد باعث افزایش قابلیت نگهداری، تستپذیری و انعطافپذیری نرمافزار میشود، زیرا از بروز خطا در کدهای پایدار جلوگیری میکند.
مثال: اشکال هندسی
فرض کنیم یک اپلیکیشن داریم که مساحت اشکال مختلف را محاسبه میکند.
در ابتدا فقط دایره (Circle) و مربع (Square) را داریم. سپس کسبوکار میخواهد مستطیل (Rectangle) را هم اضافه کند.
بر اساس اصل باز/بسته (OCP)، نباید مجبور باشیم تابع (یا ساختار) محاسبه مساحت را هر بار با افزودن یک شکل جدید تغییر دهیم.
بدون استفاده از OCP:
package main
import (
"fmt"
"math"
)
type ShapeType int
const (
Circle ShapeType = iota
Square
Rectangle
)
type Shape struct {
Type ShapeType
Radius float64
Side float64
Width float64
Height float64
}
func Area(shape Shape) float64 {
switch shape.Type {
case Circle:
return math.Pi * shape.Radius * shape.Radius
case Square:
return shape.Side * shape.Side
case Rectangle:
return shape.Width * shape.Height
default:
return 0
}
}
func main() {
circle := Shape{Type: Circle, Radius: 5}
square := Shape{Type: Square, Side: 4}
rectangle := Shape{Type: Rectangle, Width: 3, Height: 6}
fmt.Println("Circle area:", Area(circle))
fmt.Println("Square area:", Area(square))
fmt.Println("Rectangle area:", Area(rectangle))
}
مشکلات مثال بالا:
-
هر بار که یک شکل جدید اضافه میکنیم، باید تابع
Area
را تغییر دهیم — که این کار اصل باز/بسته (OCP) را نقض میکند. -
ساختار
Shape
شامل فیلدهایی است که برای برخی اشکال بیربط هستند (مثلاً فیلدSide
برای یک دایره کاربردی ندارد).
با استفاده از OCP:
package main
import (
"fmt"
"math"
)
// Shape interface is open for extension.
type Shape interface {
Area() float64
}
// Circle struct
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
// Square struct
type Square struct {
Side float64
}
func (s Square) Area() float64 {
return s.Side * s.Side
}
// Rectangle struct
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Now the area calculation function is closed for modification.
func PrintArea(shape Shape) {
fmt.Println("Shape Area:", shape.Area())
}
func main() {
circle := Circle{Radius: 5}
square := Square{Side: 4}
rectangle := Rectangle{Width: 3, Height: 6}
PrintArea(circle)
PrintArea(square)
PrintArea(rectangle)
// Suppose we add a new shape, Triangle:
// Just implement Shape and use PrintArea without changing existing code.
type Triangle struct {
Base float64
Height float64
}
func (t Triangle) Area() float64 {
return 0.5 * t.Base * t.Height
}
triangle := Triangle{Base: 5, Height: 10}
PrintArea(triangle)
}
مزایا استفاده از OCP در مثال بالا:
-
اینترفیس
Shape
برای تغییر بسته است (نیازی به تغییر آن نداریم). -
میتوانیم با ایجاد structهای جدیدی که اینترفیس
Shape
را پیادهسازی میکنند، قابلیتها را گسترش دهیم. -
تابع
PrintArea
بدون تغییر باقی میماند — حتی اگر شکلهای جدیدی مثل مثلث (Triangle)، پنجضلعی (Pentagon) یا هر شکل دیگری در آینده اضافه کنیم.
Liskov Substitution Principle (LSP)
تعریف: «اگر کلاس B زیرکلاس یا فرزند کلاس A باشد، آنگاه اشیاء کلاس B باید بتوانند جایگزین اشیاء کلاس A شوند، بدون اینکه رفتار برنامه خراب شود یا برنامه نیاز به تغییر داشته باشد.»
یعنی وقتی شما یک کلاس پایه (Base Class) دارید و از آن ارثبری میکنید، زیرکلاس شما باید طوری طراحی شود که بتواند به جای کلاس پایه استفاده شود، بدون اینکه نیازی به تغییر در کدی که از کلاس پایه استفاده میکند باشد.
در زبان گو، اینترفیسها قراردادهایی را تعریف میکنند. یک struct که ادعا میکند یک اینترفیس را پیادهسازی میکند، باید آن قرارداد را به درستی و با رفتار مورد انتظار رعایت کند.
نقض این اصل زمانی رخ میدهد که یک نوع اینترفیس را پیادهسازی کند اما قواعد یا شرایط پس از اجرا (post-conditions) آن قرارداد را رعایت نکند.
مثال: روشهای مختلف پرداخت
تصور کنید یک اینترفیس برای روشهای مختلف پرداخت دارید: کارت اعتباری، پیپال، رمزارز و غیره.
هر روش باید پرداخت را پردازش کند. اگر پیادهسازی یکی از این روشها قرارداد را نقض کند (مثلاً پول را دریافت کند اما هرگز وضعیت تراکنش را بهروزرسانی نکند)، این باعث نقض اصل LSP میشود.
بدون استفاده از LSP:
package main
import "fmt"
// اینترفیس پرداخت
type PaymentMethod interface {
ProcessPayment(amount float64) bool
UpdateTransactionStatus()
}
// کارت اعتباری
type CreditCard struct{}
func (c CreditCard) ProcessPayment(amount float64) bool {
fmt.Println("پرداخت با کارت اعتباری:", amount)
return true
}
func (c CreditCard) UpdateTransactionStatus() {
fmt.Println("آپدیت وضعیت تراکنش کارت اعتباری")
}
// پیپال که وضعیت تراکنش را آپدیت نمیکند => نقض LSP
type PayPal struct{}
func (p PayPal) ProcessPayment(amount float64) bool {
fmt.Println("پرداخت با پیپال:", amount)
return true
}
func (p PayPal) UpdateTransactionStatus() {
// این متد هیچ کاری انجام نمیدهد
}
func MakePayment(pm PaymentMethod, amount float64) {
if pm.ProcessPayment(amount) {
pm.UpdateTransactionStatus()
fmt.Println("پرداخت موفق")
} else {
fmt.Println("پرداخت ناموفق")
}
}
func main() {
cc := CreditCard{}
pp := PayPal{}
MakePayment(cc, 100) // درست کار میکند
MakePayment(pp, 200) // وضعیت تراکنش بهروزرسانی نمیشود => نقض LSP
}
با استفاده از LSP:
package main
import "fmt"
// اینترفیس پایه پرداخت فقط پردازش پرداخت
type PaymentMethod interface {
ProcessPayment(amount float64) bool
}
// اینترفیس برای پرداختهایی که وضعیت تراکنش را آپدیت میکنند
type TransactionUpdater interface {
UpdateTransactionStatus()
}
// کارت اعتباری که هر دو اینترفیس را پیادهسازی میکند
type CreditCard struct{}
func (c CreditCard) ProcessPayment(amount float64) bool {
fmt.Println("پرداخت با کارت اعتباری:", amount)
return true
}
func (c CreditCard) UpdateTransactionStatus() {
fmt.Println("آپدیت وضعیت تراکنش کارت اعتباری")
}
// پیپال فقط پردازش پرداخت را پیادهسازی میکند
type PayPal struct{}
func (p PayPal) ProcessPayment(amount float64) bool {
fmt.Println("پرداخت با پیپال:", amount)
return true
}
// تابعی که فقط پرداخت را انجام میدهد
func MakePayment(pm PaymentMethod, amount float64) {
if pm.ProcessPayment(amount) {
// اگر امکان آپدیت تراکنش بود، انجام بده
if updater, ok := pm.(TransactionUpdater); ok {
updater.UpdateTransactionStatus()
}
fmt.Println("پرداخت موفق")
} else {
fmt.Println("پرداخت ناموفق")
}
}
func main() {
cc := CreditCard{}
pp := PayPal{}
MakePayment(cc, 100) // آپدیت وضعیت تراکنش هم انجام میشود
MakePayment(pp, 200) // فقط پرداخت انجام میشود، آپدیت نیست اما نقض LSP نداریم
}
مزایا استفاده از LSP:
- افزایش انعطافپذیری کد
- کاهش خطا و باگهای منطقی
- کد قابل فهمتر و قابل نگهداریتر
Interface Segregation Principle (ISP)
تعریف: «کلاسها نباید به اینترفیس هایی وابسته باشند که از آنها استفاده نمیکنند.»
به عبارت سادهتر، به جای اینکه یک اینترفیس بزرگ و جامع داشته باشیم که شامل همه متدهای ممکن است، بهتر است آن را به چندین رابط کوچکتر و تخصصیتر تقسیم کنیم. این کار باعث میشود هر کلاس فقط آن متدهایی که واقعا نیاز دارد را پیادهسازی کند و وابستگیهای اضافه کاهش پیدا کند.
مثال: تقسیم اینترفیسها برای انواع مختلف اسناد
بدون استفاده از ISP:
type MultifunctionDevice interface {
Print(doc string) error
Scan(doc string) error
Fax(doc string) error
Email(doc string) error
}
type BasicPrinter struct{}
func (bp *BasicPrinter) Print(doc string) error {
fmt.Println("Printing doc:", doc)
return nil
}
// We must implement all methods, even if we don't use them!
func (bp *BasicPrinter) Scan(doc string) error {
return fmt.Errorf("not implemented")
}
func (bp *BasicPrinter) Fax(doc string) error {
return fmt.Errorf("not implemented")
}
func (bp *BasicPrinter) Email(doc string) error {
return fmt.Errorf("not implemented")
}
مشکلات مثال بالا:
BasicPrinter
نیازی به متدهای Scan، Fax یا Email ندارد، اما مجبور است آنها را پیادهسازی کند.- این باعث نقض اصل ISP (Interface Segregation Principle) میشود، چون
BasicPrinter
به متدهایی وابسته است که اصلاً استفاده نمیکند.
با استفاده از ISP:
type Printer interface {
Print(doc string) error
}
type Scanner interface {
Scan(doc string) error
}
type Faxer interface {
Fax(doc string) error
}
type Emailer interface {
Email(doc string) error
}
// Compose smaller interfaces to make a bigger one if needed
type MultifunctionDevice interface {
Printer
Scanner
Faxer
Emailer
}
type BasicPrinter struct{}
func (bp *BasicPrinter) Print(doc string) error {
fmt.Println("Printing doc:", doc)
return nil
}
// Now BasicPrinter doesn't implement any irrelevant methods!
type AllInOnePrinter struct{}
func (aio *AllInOnePrinter) Print(doc string) error {
fmt.Println("Printing:", doc)
return nil
}
func (aio *AllInOnePrinter) Scan(doc string) error {
fmt.Println("Scanning:", doc)
return nil
}
func (aio *AllInOnePrinter) Fax(doc string) error {
fmt.Println("Faxing:", doc)
return nil
}
func (aio *AllInOnePrinter) Email(doc string) error {
fmt.Println("Emailing:", doc)
return nil
}
مزایا استفاده از ISP:
- اینترفیسهای کوچکتر و منسجمتر (مثل Printer، Scanner، Faxer، Emailer)
BasicPrinter
فقط نیاز دارد اینترفیس Printer را پیادهسازی کند.- اما
AllInOnePrinter
میتواند کل دستگاه چندکاره را با ترکیب (embedding) این اینترفیسهای کوچکتر پیادهسازی کند.
Dependency Inversion Principle (DIP)
تعریف: «ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند. هر دو باید به انتزاعات (abstractions) وابسته باشند.»
اصل Dependency Inversion Principle (DIP) میگوید که باید به انتزاعات (interfaces) وابسته باشید، نه به پیادهسازیهای مشخص و جزئی.
این کار باعث میشود سیاستها و قوانین کسبوکار در ماژولهای سطح بالا مستقل از جزئیات اجرای آنها باشند. در نتیجه کد شما ماژولارتر و قابل تستتر خواهد شد.
ماژولهای سطح بالا در مقابل ماژولهای سطح پایین
-
ماژولهای سطح بالا:
شامل منطق تجاری (Business Logic)، موارد کاربرد (Use Cases)، و بخشهایی هستند که رفتار کلی برنامه را تعریف و مدیریت میکنند.
مثال: سرویس مدیریت کاربران، منطق سفارشدهی. -
ماژولهای سطح پایین:
شامل جزئیات فنی و پیادهسازی هستند، مانند پایگاه داده، سیستم فایل، یا سرویسهای خارجی.
مثال: کتابخانه ارتباط با دیتابیس، API برای ارسال ایمیل، ذخیره فایل در دیسک.
اگر کد سطح بالا مستقیماً یک کتابخانه پایگاه داده را ایمپورت یا نمونهسازی (instantiate) کند، کد شما به آن کتابخانه وابسته و قفل میشود.
اصل DIP پیشنهاد میکند که یک اینترفیس تعریف کنید که عملیات ذخیرهسازی را نمایش دهد.
کتابخانه واقعی پایگاه داده به عنوان یک ماژول سطح پایین، این اینترفیس را پیادهسازی میکند.
بدین ترتیب، کد سطح بالا فقط به اینترفیس وابسته است و میتوان به راحتی پیادهسازیهای مختلف را جایگزین کرد بدون تغییر کد اصلی.
مثال: ذخیرهسازی پیام در دیتابیس
بدون استفاده از DIP:
package main
import "fmt"
// Low-level module
type MySQLStorage struct{}
func (s MySQLStorage) SaveMessage(msg string) {
fmt.Println("Saving message to MySQL:", msg)
}
// High-level module
type MessageService struct {
storage MySQLStorage
}
func (ms MessageService) Send(msg string) {
ms.storage.SaveMessage(msg)
}
func main() {
service := MessageService{storage: MySQLStorage{}}
service.Send("Hello, DIP!")
}
مشکلات مثال بالا:
-
MessageService
مستقیم بهMySQLStorage
وابسته است. -
اگر بخواهیم
PostgreSQLStorage
استفاده کنیم، باید کد را تغییر دهیم → نقض DIP.
با استفاده از DIP:
package main
import "fmt"
// Abstraction
type Storage interface {
SaveMessage(msg string)
}
// Low-level module
type MySQLStorage struct{}
func (s MySQLStorage) SaveMessage(msg string) {
fmt.Println("Saving message to MySQL:", msg)
}
// High-level module
type MessageService struct {
storage Storage // وابستگی به اینترفیس
}
func (ms MessageService) Send(msg string) {
ms.storage.SaveMessage(msg)
}
func main() {
var storage Storage = MySQLStorage{}
service := MessageService{storage: storage}
service.Send("Hello, DIP with interface!")
}
مزایا استفاده از DIP:
MessageService
فقطStorage
(interface) را میشناسد.- میتوانیم به راحتی
PostgreSQLStorage
,MemoryStorage
یا هر نوع دیگری را بدون تغییرMessageService
اضافه کنیم.
اولین نفر باش که نظر ثبت میکنی :) یعنی یه کامنت به ما نمیرسه 😁