Введение: Почему 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 утверждает: «Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций».