SOLID to zestaw pięciu zasad projektowania obiektowego (w Go realizowanego przez struktury i interfejsy), mających na celu tworzenie łatwo utrzymywalnego i rozszerzalnego kodu. Stosowanie tych zasad ułatwia organizację kodu, zwiększa jego czytelność i ułatwia współpracę w zespole.
Zasada pojedynczej odpowiedzialności: Każda struktura lub pakiet powinny mieć tylko jedną odpowiedzialność, czyli jeden powód do zmiany.
// User zajmuje się zbyt wieloma rzeczami
type User struct {
Name string
Email string
DB *sql.DB
}
func (u *User) SaveToDatabase() error {
// Logika zapisu do bazy danych
return nil
}
func (u *User) SendWelcomeEmail() error {
// Logika wysyłania emaila
return nil
}
func (u *User) ValidateEmail() bool {
// Logika walidacji emaila
return strings.Contains(u.Email, "@")
}
type User struct {
Name string
Email string
}
type UserRepository struct {
db *sql.DB
}
func (r *UserRepository) Save(user *User) error {
// Logika zapisu do bazy danych
return nil
}
type EmailService struct {
smtpClient *smtp.Client
}
func (s *EmailService) SendWelcomeEmail(user *User) error {
// Logika wysyłania emaila
return nil
}
type EmailValidator struct{}
func (v *EmailValidator) Validate(email string) bool {
return strings.Contains(email, "@")
}
Zasada otwarte-zamknięte: Struktury powinny być otwarte na rozszerzanie, ale zamknięte na modyfikacje. Nową funkcjonalność należy dodawać poprzez rozszerzanie istniejącego kodu, nie modyfikując go bezpośrednio.
type PaymentProcessor struct{}
func (pp *PaymentProcessor) ProcessPayment(paymentType string, amount float64) error {
switch paymentType {
case "credit":
// Logika dla karty kredytowej
case "debit":
// Logika dla karty debetowej
case "paypal":
// Logika dla PayPal
default:
return errors.New("unknown payment type")
}
return nil
}
type PaymentMethod interface {
Process(amount float64) error
}
type CreditCardPayment struct{}
func (c *CreditCardPayment) Process(amount float64) error {
// Logika dla karty kredytowej
return nil
}
type DebitCardPayment struct{}
func (d *DebitCardPayment) Process(amount float64) error {
// Logika dla karty debetowej
return nil
}
type PayPalPayment struct{}
func (p *PayPalPayment) Process(amount float64) error {
// Logika dla PayPal
return nil
}
type PaymentProcessor struct{}
func (pp *PaymentProcessor) ProcessPayment(method PaymentMethod, amount float64) error {
return method.Process(amount)
}
Zasada podstawienia Liskov: Typy bazowe muszą być zastępowalne przez ich typy pochodne bez wpływu na poprawność działania programu. Innymi słowy, jeśli S jest podtypem T, to obiekty typu T mogą być zastąpione obiektami typu S bez zmiany poprawności programu.
type Bird interface {
Fly() string
}
type Sparrow struct{}
func (s *Sparrow) Fly() string {
return "Flying high"
}
type Penguin struct{}
func (p *Penguin) Fly() string {
panic("Can't fly!") // Łamie LSP
}
type Animal interface {
Move() string
}
type FlyingBird interface {
Animal
Fly() string
}
type SwimmingBird interface {
Animal
Swim() string
}
type Sparrow struct{}
func (s *Sparrow) Move() string {
return "Moving"
}
func (s *Sparrow) Fly() string {
return "Flying high"
}
type Penguin struct{}
func (p *Penguin) Move() string {
return "Moving"
}
func (p *Penguin) Swim() string {
return "Swimming fast"
}
Zasada segregacji interfejsów: Lepiej jest mieć wiele małych, wyspecjalizowanych interfejsów niż jeden ogólny. Klient nie powinien być zmuszony do implementacji metod, których nie używa.
type Worker interface {
Work()
Eat()
Sleep()
}
type Robot struct{}
func (r *Robot) Work() {
// Może pracować
}
func (r *Robot) Eat() {
// Robot nie je, ale musi implementować
}
func (r *Robot) Sleep() {
// Robot nie śpi, ale musi implementować
}
type Worker interface {
Work()
}
type NeedsRest interface {
Sleep()
}
type NeedsFood interface {
Eat()
}
type Human struct{}
func (h *Human) Work() {
// Implementacja pracy
}
func (h *Human) Eat() {
// Implementacja jedzenia
}
func (h *Human) Sleep() {
// Implementacja spania
}
type Robot struct{}
func (r *Robot) Work() {
// Tylko praca, bez jedzenia i spania
}
Zasada odwrócenia zależności: Moduły wysokopoziomowe nie powinny zależeć od modułów niskopoziomowych. Oba powinny zależeć od abstrakcji. Abstrakcje nie powinny zależeć od szczegółów implementacji.
type EmailSender struct{}
func (s *EmailSender) SendEmail(email, message string) error {
// Implementacja wysyłania emaila
return nil
}
type NotificationService struct {
emailSender EmailSender
}
func (n *NotificationService) Notify(email, message string) error {
return n.emailSender.SendEmail(email, message)
}
type MessageSender interface {
Send(recipient, message string) error
}
type EmailSender struct{}
func (s *EmailSender) Send(recipient, message string) error {
// Implementacja wysyłania emaila
return nil
}
type SMSSender struct{}
func (s *SMSSender) Send(recipient, message string) error {
// Implementacja wysyłania SMS
return nil
}
type NotificationService struct {
sender MessageSender
}
func NewNotificationService(sender MessageSender) *NotificationService {
return &NotificationService{sender: sender}
}
func (n *NotificationService) Notify(recipient, message string) error {
return n.sender.Send(recipient, message)
}
Stosowanie zasad SOLID w Go pomaga tworzyć kod, który jest:
Zasady SOLID są szczególnie dobrze dopasowane do idiomów Go, gdzie: