SOLID
Пять принципов объектного проектирования, которые отвечают на один вопрос: как менять код, не ломая то, что уже работает.
Что это
SOLID - аббревиатура из первых букв пяти принципов, которые собрал Роберт Мартин в начале 2000-х. По отдельности они старше: открытость/закрытость сформулировал Бертран Мейер в 1988 году, подстановку - Барбара Лисков в 1987-м. Вместе они описывают, как разложить код по модулям так, чтобы изменение требований затрагивало как можно меньше мест.
Это не правила, которые проверяет компилятор, а вопросы к коду. Почему этот класс меняется? Что сломается, если добавить ещё один вариант? Можно ли подставить другую реализацию? Сколько методов на самом деле нужно клиенту? Кто от кого зависит? Каждый принцип - один такой вопрос. Многие паттерны из каталога - готовые ответы на них.
Пять принципов
S - единственная ответственность. У модуля одна причина для изменения. Если класс меняется и когда бухгалтерия просит другой формат отчёта, и когда меняется правило подсчёта, - в нём две ответственности, и правка одной рискует сломать другую. Пример: подсчёт продаж и форматирование отчёта живут порознь, а между ними - неизменяемая сводка. Подробнее.
O - открытость/закрытость. Модуль открыт для расширения и закрыт для изменения. Новое поведение добавляется новым кодом, а не правкой проверенного. Пример: новый способ оплаты подключается к кассе отдельным классом и одной строкой при сборке, а сама касса не меняется. Подробнее.
L - подстановка Лисков. Подтип должен работать везде, где ждут базовый тип, без сюрпризов: не бросать новых исключений, не требовать большего, не обещать меньшего. Пример: хранилище только для чтения не наследует запись с "заглушкой", которая падает, а реализует только контракт чтения. Подробнее.
I - разделение интерфейсов. Клиент не должен зависеть от методов, которыми не пользуется. Толстый интерфейс заставляет реализации писать заглушки, а клиентов - знать о чужих возможностях. Пример: печать и сканирование - отдельные узкие роли, МФУ реализует обе, обычный принтер - одну. Подробнее.
D - инверсия зависимостей. Модули верхнего уровня зависят от абстракций, а детали реализуют эти абстракции. Бизнес-логика объявляет, что ей нужно, а инфраструктура подстраивается. Пример: сервис уведомлений знает порт Mailer, а SMTP подключается снаружи и подменяется в тестах. Подробнее.
Как они связаны
Принципы поддерживают друг друга. Разделённые ответственности (S) проще закрыть для изменений (O). Расширение через абстракцию (O) работает, только если реализации честно подставимы (L). Узкие интерфейсы (I) делают подстановку честной: реализовать два метода без заглушек проще, чем десять. А зависимость от абстракций (D) - механизм, на котором держатся и O, и тестируемость: если модуль сам создаёт свои зависимости, подставить другую нельзя.
Когда принципы вредят
- Абстракция без второго варианта. Интерфейс под единственную реализацию "на будущее" почти всегда оказывается не той формы, когда будущее наступает. Сначала два конкретных случая, потом обобщение.
- Дробление ради дробления. Единственная ответственность, понятая как "один метод на класс", даёт десятки файлов по пять строк, и чтобы понять одну операцию, нужно открыть их все. Ответственность - это причина изменения, а не размер.
- Расширяемость там, где её не просят. Закрытый для изменений модуль, который всё равно правят раз в неделю, - лишняя косвенность без выгоды. Иногда
matchна три ветки честнее и дешевле. - Инверсия без границы. Порт оправдан на границе с инфраструктурой (сеть, база, время) или там, где нужны тестовые двойники. Оборачивать в свой интерфейс стандартную библиотеку или собственный соседний класс - церемония.
- Принципы вместо языка. В Go, Python и TypeScript функция, протокол или структурный тип часто решают задачу без иерархии классов. Приём из Java, перенесённый буквально, в этих языках выглядит чужим.
Принципы полезны как вопросы при ревью и при изменении требований. Как чек-лист, который нужно выполнить до первой строки кода, они чаще вредят.
Связанные темы
- Стратегия - открытость/закрытость и инверсия зависимостей на одном паттерне
- Адаптер - как подключить внешнюю библиотеку к своему порту
- Репозиторий - инверсия зависимостей для хранения данных