SOLID принципы в Go: от теории к практике на Golang

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

SOLID принципы в Go: теория и практика от А до Я

SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного проектирования, сформулированных Робертом Мартином (дядюшка Боб). Несмотря на то, что Go не является классическим объектно-ориентированным языком (в нём нет классов и наследования в привычном понимании), эти принципы применимы и критически важны для создания качественного, поддерживаемого и масштабируемого кода. В этой статье мы разберём каждый принцип SOLID на примерах из реальной практики Go.

1. Single Responsibility Principle (SRP) — Принцип единственной ответственности

Суть: Каждый модуль, структура или функция должны иметь только одну причину для изменения. Другими словами, они должны отвечать за одну чётко определённую задачу.

Проблема: Часто разработчики помещают в одну структуру всю логику: валидацию, сохранение в БД, отправку email, форматирование ответа. Это приводит к «божественным объектам» (God Objects), которые сложно тестировать и изменять.

Решение в Go: Разделите ответственность между разными структурами и пакетами.

// ❌ Плохо: Одна структура делает всё
type UserService struct {
    db *sql.DB
    mailer *Mailer
}

func (s *UserService) CreateUser(name, email string) error {
    // 1. Валидация
    if !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }
    // 2. Сохранение в БД
    _, err := s.db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", name, email)
    if err != nil {
        return err
    }
    // 3. Отправка приветственного письма
    return s.mailer.Send(email, "Welcome!", "Thank you for registering")
}

// ✅ Хорошо: Разделение ответственности
type UserValidator struct{}
func (v *UserValidator) Validate(email string) error {
    if !strings.Contains(email, "@") {
        return errors.New("invalid email")
    }
    return nil
}

type UserRepository struct {
    db *sql.DB
}
func (r *UserRepository) Save(name, email string) error {
    _, err := r.db.Exec("INSERT INTO users (name, email) VALUES ($1, $2)", name, email)
    return err
}

type WelcomeMailer struct {
    mailer *Mailer
}
func (m *WelcomeMailer) Send(email string) error {
    return m.mailer.Send(email, "Welcome!", "Thank you for registering")
}

type UserService struct {
    validator *UserValidator
    repo      *UserRepository
    mailer    *WelcomeMailer
}

func (s *UserService) CreateUser(name, email string) error {
    if err := s.validator.Validate(email); err != nil {
        return err
    }
    if err := s.repo.Save(name, email); err != nil {
        return err
    }
    return s.mailer.Send(email)
}

Практический совет: Если вы не можете кратко описать, что делает ваша функция или метод, не используя союз «и», значит, вероятно, нарушен SRP.

2. Open-Closed Principle (OCP) — Принцип открытости/закрытости

Суть: Программные сущности (структуры, функции, пакеты) должны быть открыты для расширения, но закрыты для модификации. Это означает, что вы должны иметь возможность добавлять новое поведение, не изменяя существующий код.

Решение в Go: Используйте интерфейсы для абстрагирования поведения.

// ❌ Плохо: При добавлении нового типа платежа нужно менять функцию ProcessPayment
type PaymentProcessor struct{}

func (p *PaymentProcessor) Process(paymentType string, amount float64) {
    if paymentType == "credit_card" {
        // логика для кредитной карты
    } else if paymentType == "paypal" {
        // логика для PayPal
    }
    // При добавлении "crypto" — нужно добавить новый else if
}

// ✅ Хорошо: Интерфейс позволяет расширять функциональность без изменения кода
type PaymentMethod interface {
    Pay(amount float64) error
}

type CreditCard struct{}
func (c *CreditCard) Pay(amount float64) error {
    // логика обработки кредитной карты
    return nil
}

type PayPal struct{}
func (p *PayPal) Pay(amount float64) error {
    // логика обработки PayPal
    return nil
}

// Новый тип легко добавляется
type CryptoWallet struct{}
func (c *CryptoWallet) Pay(amount float64) error {
    // логика обработки криптовалюты
    return nil
}

type PaymentProcessor struct{}
func (p *PaymentProcessor) Process(method PaymentMethod, amount float64) error {
    return method.Pay(amount)
}

Ключевой момент: В Go интерфейсы реализуются неявно. Это делает OCP особенно удобным — вы можете создавать новые реализации, не трогая существующие структуры.

3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Суть: Объекты в программе должны быть заменяемыми на экземпляры их подтипов без нарушения корректности программы. В контексте Go это означает, что любая реализация интерфейса должна корректно работать там, где ожидается этот интерфейс.

Типичное нарушение: Создание интерфейса, который подразумевает определённое поведение, но некоторые реализации не поддерживают его полностью.

// ❌ Плохо: Нарушение LSP
type Bird interface {
    Fly() error
}

type Sparrow struct{}
func (s *Sparrow) Fly() error {
    return nil // Воробей летает
}

type Penguin struct{}
func (p *Penguin) Fly() error {
    return errors.New("penguins can't fly") // Пингвин не летает — нарушение!
}

// ✅ Хорошо: Разделение интерфейсов
type Flyer interface {
    Fly() error
}

type Swimmer interface {
    Swim() error
}

type Sparrow struct{}
func (s *Sparrow) Fly() error { return nil }

type Penguin struct{}
func (p *Penguin) Swim() error { return nil }
func (p *Penguin) Fly() error  { return nil } // Пингвин может "летать" в воде

// Теперь функция принимает только тех, кто действительно летает
func LetItFly(f Flyer) error {
    return f.Fly()
}

Правило: Если реализация интерфейса возвращает ошибку «не поддерживается» или паникует — это почти всегда нарушение LSP.

4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса

Суть: Клиенты не должны зависеть от методов, которые они не используют. Лучше иметь несколько специализированных интерфейсов, чем один «толстый».

Решение в Go: Go поощряет создание маленьких, сфокусированных интерфейсов из 1-2 методов.

// ❌ Плохо: Толстый интерфейс                

Блоги

Рекомендаций по книгам