SOLID принципы в Go: теория и практика для разработчиков

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

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

Начать курс

Введение: Почему SOLID важен в Go?

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

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

SRP гласит: «У модуля должна быть только одна причина для изменения». В Go это означает, что функция, структура или пакет должны отвечать за одну чётко определённую задачу.

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

type Report struct {
    Title string
    Body  string
}

func (r *Report) Generate() string {
    return fmt.Sprintf("<h1>%s</h1><p>%s</p>", r.Title, r.Body)
}

func (r *Report) SaveToFile(filename string) error {
    return os.WriteFile(filename, []byte(r.Generate()), 0644)
}

func (r *Report) SendEmail(addr string) error {
    // логика отправки email
    return nil
}

Здесь структура Report отвечает за генерацию, сохранение и отправку — три разные причины для изменения.

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

type Report struct {
    Title string
    Body  string
}

func (r *Report) Generate() string {
    return fmt.Sprintf("<h1>%s</h1><p>%s</p>", r.Title, r.Body)
}

type ReportSaver struct{}

func (rs *ReportSaver) SaveToFile(report *Report, filename string) error {
    return os.WriteFile(filename, []byte(report.Generate()), 0644)
}

type EmailSender struct{}

func (es *EmailSender) Send(report *Report, addr string) error {
    // логика отправки email
    return nil
}

Теперь каждая сущность имеет одну ответственность: генерация, сохранение и отправка.

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

OCP утверждает: «Программные сущности должны быть открыты для расширения, но закрыты для модификации». В Go это достигается через интерфейсы и композицию.

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

type PaymentProcessor struct{}

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

При добавлении нового типа платежа придётся модифицировать метод Process.

Хороший пример (соблюдение 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
}

type PaymentProcessor struct{}

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

Теперь для добавления нового способа оплаты достаточно реализовать интерфейс PaymentMethod, не трогая существующий код.

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

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

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

type Bird interface {
    Fly() error
}

type Penguin struct{}
func (p *Penguin) Fly() error {
    return errors.New("пингвины не летают")
}

func LetBirdFly(b Bird) {
    err := b.Fly()
    if err != nil {
        log.Fatal(err)
    }
}

Пингвин реализует интерфейс Bird, но не может летать — нарушается контракт.

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

type Bird interface {
    Move() error
}

type FlyingBird struct{}
func (f *FlyingBird) Move() error {
    // летит
    return nil
}

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

func MoveBird(b Bird) {
    b.Move()
}

Интерфейс Bird теперь абстрактно описывает движение, а не конкретный способ. Оба подтипа корректно его реализуют.

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

ISP гласит: «Клиенты не должны зависеть от интерфейсов, которые они не используют». В Go это означает создание маленьких, специализированных интерфейсов.

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

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

type Human struct{}
func (h *Human) Work() { /* работает */ }
func (h *Human) Eat()  { /* ест */ }
func (h *Human) Sleep(){ /* спит */ }

type Robot struct{}
func (r *Robot) Work() { /* работает */ }
func (r *Robot) Eat()  { /* не ест */ panic("not implemented") }
func (r *Robot) Sleep(){ /* не спит */ panic("not implemented") }

Робот вынужден реализовывать методы, которые ему не нужны.

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

type Workable interface {
    Work()
}

type Eatable interface {
    Eat()
}

type Sleepable interface {
    Sleep()
}

type Human struct{}
func (h *Human) Work() { /* работает */ }
func (h *Human) Eat()  { /* ест */ }
func (h *Human) Sleep(){ /* спит */ }

type Robot struct{}
func (r *Robot) Work() { /* работает */ }

Каждый тип реализует только те интерфейсы, которые ему нужны.

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

DIP утверждает: «Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций».

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

Блоги

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