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)
}

Типовые вопросы

  1. Почему SRP не равен «один класс — одна функция»?
    • Единица ответственности — причина изменения для заинтересованной стороны, а не размер файла.
  2. Когда OCP оправдан?
    • Когда варианты действительно появляются независимо и контракт между ними стабилен; иначе простая ветка дешевле.
  3. Как увидеть нарушение LSP до production?
    • Описать предусловия и постусловия интерфейса, затем прогнать одинаковые контрактные тесты для реализаций.
  4. Почему широкий интерфейс опасен?
    • Он расширяет связность, усложняет фейки и вынуждает клиентов зависеть от лишнего.
  5. Всегда ли DIP требует DI-фреймворка?
    • Нет. В Go достаточно передать зависимость через конструктор или поле; контейнер не нужен.
  6. Что делать с legacy-модулем, нарушающим все принципы?
    • Выбрать ближайшее изменение, окружить его тестами и извлекать границы инкрементально, не переписывая всё сразу.

Практика

  • Возьмите функцию оформления заказа, где есть расчёт цены, запись в БД и отправка уведомления.
    • Критерии готовности: расчёт вынесен в чистую функцию; отправитель описан минимальным интерфейсом у потребителя; тесты проверяют успешную оплату и ошибку отправки.
  • Добавьте второй канал уведомлений.
    • Критерии готовности: существующий сценарий проходит без изменения бизнес-алгоритма; новая реализация проходит те же контрактные тесты; нет метода-заглушки «не поддерживается».

Частые ошибки и ловушки

  • Создавать интерфейс для каждой структуры до появления потребителя или альтернативной реализации.
  • Называть SRP поводом дробить связанную транзакционную логику между пакетами.
  • Считать наследование или embedding автоматическим соблюдением LSP.
  • Прятать сложный порядок правил за паттерном Strategy, когда читабельный switch честнее.
  • Мокать всё подряд вместо проверки наблюдаемого результата и отдельных контрактов.

Связанные темы

Источники