SOLID принципы в Go: продвинутый уровень
SOLID — это пять фундаментальных принципов объектно-ориентированного проектирования, которые помогают создавать гибкий, поддерживаемый и масштабируемый код. В контексте Go, языка с уникальной философией композиции вместо наследования, применение SOLID требует особого подхода. В этой статье мы разберем каждый принцип на продвинутом уровне, с реальными примерами и антипаттернами, характерными для Go-сообщества.
1. Single Responsibility Principle (SRP) в Go
Принцип единственной ответственности гласит: у класса (структуры) должна быть только одна причина для изменения. В Go это часто нарушается в утилитарных пакетах и "божественных объектах".
Антипаттерн: структура с множеством обязанностей
type UserService struct {
db *sql.DB
cache *redis.Client
logger *log.Logger
}
func (s *UserService) CreateUser(ctx context.Context, u User) error {
// 1. Валидация
// 2. Хеширование пароля
// 3. Сохранение в БД
// 4. Отправка email
// 5. Логирование
return nil
}
Решение: разделите ответственности на отдельные сервисы и используйте композицию.
type UserValidator interface {
Validate(u User) error
}
type PasswordHasher interface {
Hash(password string) (string, error)
}
type UserRepository interface {
Save(ctx context.Context, u User) error
}
type EmailSender interface {
SendWelcomeEmail(ctx context.Context, email string) error
}
type UserService struct {
validator UserValidator
hasher PasswordHasher
repo UserRepository
emailSender EmailSender
logger Logger
}
Каждый компонент теперь имеет одну причину для изменения, что упрощает тестирование и поддержку.
2. Open-Closed Principle (OCP) через интерфейсы
Принцип открытости/закрытости: сущности должны быть открыты для расширения, но закрыты для модификации. В Go это достигается через интерфейсы и внедрение зависимостей.
Плохой пример: жесткое связывание
type PaymentProcessor struct {
paypalAPI *PayPalAPI
}
func (p *PaymentProcessor) Process(amount float64) error {
return p.paypalAPI.Charge(amount)
}
Если нужно добавить Stripe — придется менять код.
Правильный подход: интерфейс + стратегия
type PaymentGateway interface {
Charge(amount float64) error
Refund(transactionID string) error
}
type PayPalGateway struct{ /* ... */ }
type StripeGateway struct{ /* ... */ }
func (p *PayPalGateway) Charge(amount float64) error { /* ... */ }
func (s *StripeGateway) Charge(amount float64) error { /* ... */ }
type PaymentService struct {
gateway PaymentGateway
}
func NewPaymentService(gw PaymentGateway) *PaymentService {
return &PaymentService{gateway: gw}
}
Теперь можно добавлять новые платежные шлюзы без изменения PaymentService.
3. Liskov Substitution Principle (LSP) в контексте Go
Принцип подстановки Барбары Лисков: объекты в программе должны быть заменяемы экземплярами их подтипов без изменения корректности программы. В Go, где нет классического наследования, LSP проявляется через контракты интерфейсов.
Типичное нарушение: интерфейс с избыточными методами
type Bird interface {
Fly() error
Swim() error
}
type Penguin struct{}
func (p *Penguin) Fly() error {
return errors.New("penguins can't fly")
}
func (p *Penguin) Swim() error {
return nil
}
Здесь Penguin не может корректно реализовать Fly(), что нарушает LSP.
Решение: разделение интерфейсов
type Flyer interface {
Fly() error
}
type Swimmer interface {
Swim() error
}
type Duck struct{}
func (d *Duck) Fly() error { return nil }
func (d *Duck) Swim() error { return nil }
type Penguin struct{}
func (p *Penguin) Swim() error { return nil }
Теперь Penguin реализует только Swimmer, и код, ожидающий Flyer, не сломается.
4. Interface Segregation Principle (ISP) на практике
Принцип разделения интерфейса: клиенты не должны зависеть от методов, которые они не используют. В Go это особенно важно, так как интерфейсы — основной механизм абстракции.
Толстый интерфейс (anti-pattern)
type Worker interface {
Work() error
Eat() error
Sleep() error
}
type Robot struct{}
func (r *Robot) Work() error { return nil }
func (r *Robot) Eat() error { return errors.New("robots don't eat") }
func (r *Robot) Sleep() error { return errors.New("robots don't sleep") }
Правильный подход: тонкие интерфейсы
type Worker interface {
Work() error
}
type Eater interface {
Eat() error
}
type Sleeper 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 }
Клиентский код теперь запрашивает только необходимые интерфейсы:
func ProcessWorkers(workers []Worker) {
for _, w := range workers {
w.Work()
}
}
5. Dependency Inversion Principle (DIP) и внедрение зависимостей
Принцип инверсии зависимостей: модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
Нарушение: прямая зависимость от конкретной реализации
type MySQLRepository struct{}
func (r *MySQLRepository) GetUser(id int) User {
// прямой запрос к MySQL
}
type UserController struct {
repo *MySQLRepository
}
func (c *UserController) GetUser(id int) User {
return c.repo.GetUser(id)
}
Правильная реализация с DIP
type UserRepository interface {
GetUser(id int) (User, error)
SaveUser(u User) error
}
type MySQLRepository struct{}
func (r *MySQLRepository) GetUser(id int) (User, error) {
// реализация
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &