SOLID на практике
Зачем это на интервью
SOLID проверяют не как список расшифровок, а как способность безопасно менять код. На интервью E4 важно показать, где в конкретном сервисе смешались причины изменения, куда поставить границу и как доказать, что поведение не сломалось. Полезный ответ начинается с сценария изменения: «добавляем новый способ оплаты», а не с абстрактного «нужно применять OCP».
Минимум для E4
- Объяснить SRP, OCP, LSP, ISP и DIP своими словами и привести по одному признаку нарушения.
- Выделить зависимость от инфраструктуры за маленьким интерфейсом в потребляющем пакете.
- Сохранить бизнес-инварианты при расширении сценария и покрыть их тестами.
- Уметь отказаться от принципа, если он добавляет абстракцию без второго варианта поведения.
SRP (single responsibility): у модуля должна быть одна осмысленная причина измениться. OrderService, одновременно рассчитывающий скидку, пишет SQL и отправляет письмо, меняется по трём независимым причинам. Разделение не означает «один метод — один файл»: ответственность определяется ролью в предметной области.
OCP (open/closed): новое допустимое поведение добавляют через расширение, не переписывая стабильный алгоритм. Например, правила скидок можно представить функциями, но не стоит строить реестр плагинов, пока нет вариативности.
LSP (Liskov substitution): реализация должна выполнять контракт, ожидаемый клиентом. Если ReadonlyStore при вызове Save паникует, это плохая подстановка Store; лучше не включать запись в его контракт.
ISP (interface segregation): клиент зависит только от нужных операций. Интерфейс из 12 методов повышает стоимость фейков и заставляет реализации поддерживать неиспользуемые возможности.
DIP (dependency inversion): политика высокого уровня зависит от абстракции, а деталь базы данных или HTTP зависит от неё же. Направление импорта само по себе не цель: важен контроль бизнес-кодом над контрактом.
Углубление для E5/Senior
E5 связывает принципы с экономикой изменений. Он оценивает частоту изменения, число команд-владельцев, риск совместимости и наблюдаемость, а затем выбирает гранулярность границ. Хорошее решение может временно нарушать SRP в маленьком пакете, если разделение создало бы координационную стоимость.
Для устойчивого контракта фиксируйте не только сигнатуру, но и семантику: идемпотентность, порядок вызовов, ошибки, дедлайны и допустимые nil. LSP проверяется контрактными тестами, запускаемыми для каждой реализации. OCP полезен, когда расширения независимы; если правила конкурируют за общий порядок, явный switch иногда понятнее цепочки обработчиков.
Ключевые понятия
| Принцип | Вопрос при ревью | Практический сигнал |
|---|---|---|
| SRP | Кто и почему меняет этот код? | Несвязанные изменения в одном PR |
| OCP | Как добавить вариант без риска для старых? | Для независимого варианта приходится менять стабильный алгоритм в нескольких местах |
| LSP | Может ли клиент не знать конкретную реализацию? | Паникующие или «неподдерживаемые» методы |
| ISP | Какие методы реально нужны клиенту? | Тяжёлые моки и пустые заглушки |
| DIP | Кто владеет контрактом зависимости? | Бизнес-слой импортирует драйвер БД |
В Go интерфейсы обычно объявляют рядом с потребителем. Это оставляет контракт маленьким и позволяет адаптеру реализовать его структурно.
type ReceiptSender interface {
Send(ctx context.Context, receipt Receipt) error
}
type Checkout struct {
sender ReceiptSender
}
func (c Checkout) Complete(ctx context.Context, receipt Receipt) error {
return c.sender.Send(ctx, receipt)
}Типовые вопросы
- Почему SRP не равен «один класс — одна функция»?
- Единица ответственности — причина изменения для заинтересованной стороны, а не размер файла.
- Когда OCP оправдан?
- Когда варианты действительно появляются независимо и контракт между ними стабилен; иначе простая ветка дешевле.
- Как увидеть нарушение LSP до production?
- Описать предусловия и постусловия интерфейса, затем прогнать одинаковые контрактные тесты для реализаций.
- Почему широкий интерфейс опасен?
- Он расширяет связность, усложняет фейки и вынуждает клиентов зависеть от лишнего.
- Всегда ли DIP требует DI-фреймворка?
- Нет. В Go достаточно передать зависимость через конструктор или поле; контейнер не нужен.
- Что делать с legacy-модулем, нарушающим все принципы?
- Выбрать ближайшее изменение, окружить его тестами и извлекать границы инкрементально, не переписывая всё сразу.
Практика
- Возьмите функцию оформления заказа, где есть расчёт цены, запись в БД и отправка уведомления.
- Критерии готовности: расчёт вынесен в чистую функцию; отправитель описан минимальным интерфейсом у потребителя; тесты проверяют успешную оплату и ошибку отправки.
- Добавьте второй канал уведомлений.
- Критерии готовности: существующий сценарий проходит без изменения бизнес-алгоритма; новая реализация проходит те же контрактные тесты; нет метода-заглушки «не поддерживается».
Частые ошибки и ловушки
- Создавать интерфейс для каждой структуры до появления потребителя или альтернативной реализации.
- Называть SRP поводом дробить связанную транзакционную логику между пакетами.
- Считать наследование или embedding автоматическим соблюдением LSP.
- Прятать сложный порядок правил за паттерном Strategy, когда читабельный
switchчестнее. - Мокать всё подряд вместо проверки наблюдаемого результата и отдельных контрактов.
Связанные темы
Источники
- Robert C. Martin. Agile Software Development: Principles, Patterns, and Practices, 2002.
- Go Code Review Comments: Interfaces.
- Effective Go: Interfaces and other types.