Боль [S]OLID 19.12.2023

SOLID — это набор ни к чему не обязывающих рекомендаций. Ваше дело, пользоваться ими или не пользоваться. Спасибо за внимание!

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

SRP или Single Responsibility Principle многие трактуют как принцип единой ответственности. Ну вот же, написано черным по белому.

Роберт Мартин, который объединил множество описанных разными людьми подходов, назвал получившееся именно термином Single responsibility principle. Причем забавно, но он подозревает, что украл это название у другого автора. Спустя несколько десятков лет Роберт Мартин пишет в своем блоге, что сожалеет о выбранном названии, потому что понимание этого принципа и среди англоязычных программистов такое же искаженное. А все из-за названия.

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

Хочу обратить особое внимание на слово модуль. Часто я встречал подмену этого понятия на класс, что не является корректным. В контексте SRP модуль — это некий организованный блок кода. Он может представлять из себя в том числе набор классов, объединенных для решения ровно одной задачи. Он может представлять из себя даже одну конкретную функцию. Важно понимать, что этот код должен быть организован именно так, чтобы изменялся этот код только лишь по одной причине.

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

Очень очевидный пример

Давайте рассмотрим первую ситуацию. Бизнес при создании заказа решает ввести уведомление покупателю об этом созданном заказе, должны ли мы вносить изменения в код создания заказа?

Нет, потому что создание заказа имеет только одну причину изменения, связанную с созданием заказа. Изменение набора полей, дополнительные условия для создания заказа в верном инварианте — вот такие изменения.

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

Не очень очевидный пример

Давайте рассмотрим вторую ситуацию. У нас есть калькулятор финальной цены товара, в формулу которого требуется внедрить дополнительную переменную в виде суммы скидки, чтобы соответствовать новым требованиям бизнеса. Должны ли мы изменять сам калькулятор?

Ответ далеко не такой однозначный и зависит от того, с какой стороны взглянуть на модуль. Уверен, разные разработчики дадут разные ответы, особенно, если начнут рассуждать.

Этот пример очень хорош, он показывает всю неоднозначность принципа. Пока мы вносим изменения в формулу расчета финальной цены товара, вопросов нет. Все, что относится к изменению этой формулы, относится к этому же модулю. А что, если мы вносим изменение в формулу или принцип расчета суммы скидки?

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

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

Выбор трактования зависит как от мнения самого разработчика, так и от того, как именно мы рассматриваем эту бизнес-функциональность в приближении. Здесь нет единственного верного толкования.

Так что делать?

Стоит учитывать, что SRP базируется на одном из шаблонов GRASP: модуль внутри себя обязан соблюдать сильное сцепление (high cohesion), а модули друг с другом должны быть связаны слабо (low coupling). Выбор того, отделять ли в моем примере расчет скидки от расчета финальной цены, ложится на плечи разработчиков и договоренностей между ними.

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

Почему? Можно порассуждать: если дополняется формула финальной цены товара, которая также начинает учитывать какой-нибудь сезонный коэффициент, то логика расчета скидки ведь не изменяется. Мы не вносим никаких изменений в правила расчета скидок, мы в принципе скидки не трогаем. А значит логика расчета скидки для меня здесь является отдельным модулем.

Послесловие

Этот принцип, как и остальные из SOLID, является рекомендацией, а не призывом к обязательному действию. Он помогает рассуждать и принимать более верные решения при организации связей между компонентами приложения.

Следование этому принципу может зависеть от размера приложения, сложности бизнес-функциональности и даже команды разработчиков.