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

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

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

Начать курс

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

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

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

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

Практика в Go: Частая ошибка — создание «божественных» структур, которые занимаются и бизнес-логикой, и логированием, и отправкой уведомлений.

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

type OrderService struct{}

func (s *OrderService) Create(order Order) error {
    // 1. Сохранение в БД
    // 2. Отправка email
    // 3. Логирование
    return nil
}

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

type OrderRepository interface {
    Save(order Order) error
}

type EmailService interface {
    Send(to string, subject string, body string) error
}

type Logger interface {
    Info(msg string)
}

type OrderService struct {
    repo  OrderRepository
    email EmailService
    log   Logger
}

func (s *OrderService) Create(order Order) error {
    if err := s.repo.Save(order); err != nil {
        s.log.Info("Failed to save order")
        return err
    }
    if err := s.email.Send(order.Email, "Order Confirmed", "Your order is placed"); err != nil {
        s.log.Info("Failed to send email")
    }
    s.log.Info("Order created successfully")
    return nil
}

Теперь каждая структура отвечает только за свою задачу, и изменение логики email не повлияет на сохранение заказов.

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

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

Практика в Go: Используйте интерфейсы и композицию, чтобы добавлять новые реализации без изменения основной логики.

Пример: Предположим, у нас есть система расчёта стоимости доставки. Изначально есть только «обычная доставка», но нужно добавить «экспресс-доставку».

// Определяем интерфейс
type ShippingCostCalculator interface {
    Calculate(order Order) float64
}

// Стандартная реализация
type StandardShipping struct{}

func (s StandardShipping) Calculate(order Order) float64 {
    return 5.99
}

// Новая реализация без изменения кода выше
type ExpressShipping struct{}

func (e ExpressShipping) Calculate(order Order) float64 {
    return 15.99
}

// Использование
func ProcessOrder(order Order, calculator ShippingCostCalculator) {
    cost := calculator.Calculate(order)
    fmt.Printf("Shipping cost: %.2f\n", cost)
}

Мы не меняем ProcessOrder — просто передаём новый экземпляр ExpressShipping. Это и есть OCP.

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

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

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

type Shape interface {
    Area() float64
}

type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

type Square struct {
    Side float64
}

func (s Square) Area() float64 {
    return s.Side * s.Side
}

// Функция ожидает Shape, подставить можно и Rectangle, и Square
func PrintArea(s Shape) {
    fmt.Printf("Area: %.2f\n", s.Area())
}

Здесь LSP соблюдён: и Rectangle, и Square корректно реализуют Area(), и PrintArea работает с обоими.

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

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

Практика в Go: В Go интерфейсы по умолчанию маленькие (часто 1-3 метода). Не бойтесь создавать много маленьких интерфейсов.

Плохой пример (толстый интерфейс):

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

Если у вас есть робот, который только работает, он вынужден реализовывать Eat() и Sleep(), что бессмысленно.

Хороший пример (разделение):

type Worker interface {
    Work()
}

type Eater interface {
    Eat()
}

type Sleeper interface {
    Sleep()
}

type Robot struct{}

func (r Robot) Work() { /* ... */ }

type Human struct{}

func (h Human) Work() { /* ... */ }
func (h Human) Eat()  { /* ... */ }
func (h Human) Sleep() { /* ... */ }

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

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

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

Практика в Go: Внедряйте зависимости через интерфейсы, а не через конкретные типы. Это позволяет легко менять реализацию (например, для тестирования).

// Абстракция (интерфейс)
type Database interface {
    Save(user User) error
}                

Блоги

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