SOLID

Полнее всего SOLID раскрывается в материалах Роберта Мартина:

SOLID - набор принципов, следование которым сделает систему поддерживаемой и расширяемой. Они перекликаются с идеями Agile, Роберт Мартин один из авторов Agile manifesto.

The Single Responsibility Principle (SRP)

Формулировка: A module should be responsible to one, and only one, actor.

Под модулем тут понимается модуль/класс/неймспейс/файл. Под ответственностью понимается, что создание этого модуля, его редактирование и удаление контролирует одно действующее лицо. Это действующее лицо - не программист, а бизнес.

Допустим, мы делаем карточную игру. На проекте есть действующее лицо, например аналитик, которое формулирует правила игры. Есть дизайнер, который отвечает за внешний вид стола, карт, игроков. Есть администратор баз данных, который проектирует схему базы данных и запросы к ней. Согласно этому принципу не должно быть модуля, который изменяется из-за требований разных действующих лиц. Все изменения в этом модуле может запрашивать только одна роль.

Очевидно, что ролей может быть больше. Например, есть редактор-переводчик, отвечающий за текстовые сообщения в игре, в том числе на разных языках. В этом случае UI не может содержать текст, и обязательно использование движков локализации, хранящих переводы отдельно от кода UI.

Исходя из своего опыта вы можете представлять одного человека как несколько действующих лиц. Например, один и тот же человек в команде отвечает за правила в игре и дизайн. Но вы понимаете, что проект будет разрабатываться долго и вероятно, что дизайном займется отдельный человек, по этому вы трактуете требования одного аналитика как требования 2х действующих лиц. Или вы уверены, что локализация приложения не потребуется, а ваша команда не имеет опыта локализации, тогда вы принимаете решение что код UI будет содержать текст сообщений.

The Open Closed Principle (OCP)

Формулировка: A software artifact should be open for extension but closed for modification.

Под software artifact тут понимается файл с исходным текстом, бинарный файл вроде jar или dll, пакет вроде gem или npm.

Речь идет от том, что без модификации артефакта мы можем менять его поведение. Например, артефакт содержит код рассылки писем клиентам. Но объект, рассылающий письма, ничего не знает о способе доставки. И артефакт не содержит кода доставки. Будет ли это SMTP сервер или сервис, или SMS мы решим когда воспользуемся этим артефактом.

Фактически речь идет о плагинах.

Опять-таки, это не правило, а принцип. Чтобы сделать действительно расширяемую систему мы должны знать будущие требования. Исходя из своего опыта мы можем предвидеть будущие требования и сделать систему гибкой в нужных местах. При этом мы должны учитывать, что обеспечение гибкости имеет свою цену.

The Liskov Substitution Principle (LSP)

Имеет сложное математическое определение, которое можно заменить на: Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.

Компилятор не может автоматически проверить то, что кто-то нарушит контракт, поэтому эта проверка возлагается на разработчика.

Например, есть класс Stack, реализующий следующий интерфейс: length, push, pop. И вы ожидаете, что любой стек увеличивает длину на единицу, если в него что-то положили. Некто создает потомка DoubleStack, который дублирует добавляемые элементы. С точки зрения компилятора, функции, работающие со Stack, могут работать и с DoubleStack, но определенно, что эти функции не будут работать корректно с этим потомком.

Это относиться не только к наследованию. Производный тип можно получить, например с помощью паттерна декоратор.

The Interface Segregation Principle (ISP)

Формулировка: Make fine grained interfaces that are client specific.

Можно перевести как: Клиенты не должны зависеть от методов, которые они не используют.

Этот принцип относится только к тем языкам, которые содержат концепции подобные абстрактным классам и интерфейсам. Например, в Clojure это протоколы.

DIP: The Dependency Inversion Principle

Формулировка: Depend on abstractions, not on concretions.

  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Что такое модули верхних уровней? Как определить этот уровень? Чем ближе модуль к вводу/выводу, тем ниже уровень модуля. Т.е. модули низкого уровня работают с базой данных, интерфейсом пользователя, сокетами и т.п. А модули, реализующие бизнес-логику — высокого уровня.

Зависимость модулей - это ссылка на модуль в исходном коде, т.е. import, require и т.п. Но как бизнес-логика не будет зависеть от модулей работы с базой данных? Важно разделять compile-time и runtime зависимости. Да, в runtime бизнес-логика обязательно использует модули взаимодействия с базой, но исходный код, содержащий бизнес-логику, не ссылается напрямую на модуль низкого уровня. Такой трюк возможен благодаря динамическому полиморфизму.

Есть модуль Logic, реализующий логику, который должен отсылать уведомления. В этом же пакете объявляется интерфейс ISender, который используется Logic. Уровнем ниже, в другом пакете объявляется ConcreteSender, реализующий ISender. Получается, что в момент компиляции Logic не зависит от ConcreteSender. В runtime, мы можем настроить Logic так, чтобы он работал с ConcreteSender. Как правило, конструктор класса Logic позволяет передать экземпляр ConcreteSender.

Такое направление зависимостей позволяет начать разработку с самого главного - с высокоуровневых правил. При этом мы должны представлять, какими будут модули нижних уровней, чтобы правильно выбрать абстракции. Мы должны решить, будет ли наш проект использовать key-value хранилище или это будет реляционная база, будет ли оно поддерживать ACID транзакции, способно ли оно выполнять полнотекстовые запросы, будет ли использоваться шардирование.

Начиная с модулей верхнего уровня мы можем корректно подсчитать расходы. Допустим, мы не следуем этому принципу и на проект затрачено 100 монет и мы не можем сказать сколько из них потрачено на бизнес-правила, а сколько на интерфейс. Но если мы следуем этому правилу, то после реализации бизнес-правил можно подсчитать затраты, допустим, потрачено 10 монет. Соответственно, бизнес может решить не тратить в следующий раз 90 монет на интерфейс.

Резюме

SOLID - набор принципов, а не правил. Нужно осознано идти на нарушение этих принципов, ведь абстракции имеют свою цену. Взять тот же OCP, не стоит все делать расширяемым, жизнь все равно поставит задачу, для которой система будет не расширяемой. Нет серебряной пули, именно поэтому нам платят деньги за поиск баланса и принятие технических решений.