Zasady SOLID w Go

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.

1. Single Responsibility Principle (SRP)

Zasada pojedynczej odpowiedzialności: Każda struktura lub pakiet powinny mieć tylko jedną odpowiedzialność, czyli jeden powód do zmiany.

Przykład błędny

// 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, "@")
}

Przykład poprawny

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, "@")
}

2. Open/Closed Principle (OCP)

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.

Przykład błędny

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
}

Przykład poprawny

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)
}

3. Liskov Substitution Principle (LSP)

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.

Przykład błędny

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
}

Przykład poprawny

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"
}

4. Interface Segregation Principle (ISP)

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.

Przykład błędny

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ć
}

Przykład poprawny

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
}

5. Dependency Inversion Principle (DIP)

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.

Przykład błędny

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)
}

Przykład poprawny

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)
}

Podsumowanie

Stosowanie zasad SOLID w Go pomaga tworzyć kod, który jest:

Zasady SOLID są szczególnie dobrze dopasowane do idiomów Go, gdzie: