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 методов.
// ❌ Плохо: Толстый интерфейс