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

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

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

Начать курс

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

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

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

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

В Go это часто нарушается, когда структура (struct) выполняет слишком много задач: работает с базой данных, отправляет email, формирует отчёты.

Плохой пример (нарушение SRP):

type User struct {
    Name  string
    Email string
}

func (u *User) Save() error {
    // логика сохранения в БД
    return nil
}

func (u *User) SendWelcomeEmail() error {
    // логика отправки email
    return nil
}

func (u *User) GenerateReport() string {
    // логика формирования отчёта
    return "report"
}

Хороший пример (соблюдение SRP):

type User struct {
    Name  string
    Email string
}

type UserRepository struct{}

func (r *UserRepository) Save(u *User) error {
    // только сохранение
    return nil
}

type EmailService struct{}

func (s *EmailService) SendWelcome(u *User) error {
    // только отправка email
    return nil
}

type ReportGenerator struct{}

func (g *ReportGenerator) Generate(u *User) string {
    // только генерация отчёта
    return "report"
}

Вывод: Каждая структура отвечает за свою задачу. Изменение логики сохранения не затронет отправку писем.

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

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

В Go это отлично реализуется через интерфейсы.

Плохой пример (нарушение OCP):

type PaymentProcessor struct{}

func (p *PaymentProcessor) Process(paymentType string, amount float64) {
    if paymentType == "credit_card" {
        // логика для кредитной карты
    } else if paymentType == "paypal" {
        // логика для PayPal
    }
    // при добавлении нового типа придётся менять этот метод
}

Хороший пример (соблюдение OCP):

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
}

func ProcessPayment(method PaymentMethod, amount float64) error {
    return method.Pay(amount)
}

Вывод: Теперь, чтобы добавить новый способ оплаты (например, ApplePay), достаточно создать новую структуру с методом Pay(), не трогая существующий код.

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 fmt.Errorf("penguin cannot fly")
}

Если мы напишем функцию, которая вызывает Fly() для любой птицы, то пингвин сломает логику.

Хороший пример (соблюдение LSP):

type Bird interface {
    Move() error
}

type Sparrow struct{}

func (s *Sparrow) Move() error {
    // летит
    return nil
}

type Penguin struct{}

func (p *Penguin) Move() error {
    // плавает или ходит
    return nil
}

Вывод: Интерфейс Bird теперь описывает общее поведение "передвижение", которое корректно реализуется всеми подтипами.

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

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

Плохой пример (нарушение ISP):

type Worker interface {
    Work() error
    Eat() error
    Sleep() error
}

type Human struct{}

func (h *Human) Work() error { return nil }
func (h *Human) Eat() error  { return nil }
func (h *Human) Sleep() error { return nil }

type Robot struct{}

func (r *Robot) Work() error { return nil }
func (r *Robot) Eat() error  { return fmt.Errorf("robot does not eat") }
func (r *Robot) Sleep() error { return fmt.Errorf("robot does not sleep") }

Хороший пример (соблюдение ISP):

type Workable interface {
    Work() error
}

type Eatable interface {
    Eat() error
}

type Sleepable interface {
    Sleep() error
}

type Human struct{}

func (h *Human) Work() error  { return nil }
func (h *Human) Eat() error   { return nil }
func (h *Human) Sleep() error { return nil }

type Robot struct{}

func (r *Robot) Work() error { return nil }
// Robot не реализует Eat и Sleep — и это нормально

Вывод: Маленькие интерфейсы проще реализовывать и тестировать. Робот не обязан уметь есть.

5. Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Суть: Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций (интерфейсов). Абстракции

Блоги

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