SOLID в деталях: Принцип единственной ответственности

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

Принцип единственной ответственности

Читая большое количество тематической литературы, будь то хоть книги, хоть статьи в Интернете, я не мог не обратить своё внимание на тот факт, что принцип единственной ответственности (Single Responsibility Principle; SRP) имеет самое большое количество трактовок из всех принципов SOLID. Причиной этому является очень расплывчатое определение принципа, данное Робертом Мартином, которое звучит следующим образом: «Класс должен иметь только одну причину для изменения» («A class should have only one reason to change»).

Однако и сам Дядюшка Боб признаёт факт трудности понимания этого принципа в своей книге «Чистая Архитектура» и предлагает более точные его формулировки: «Модуль должен отвечать за одного и только одного пользователя или заинтересованное лицо» и «Модуль должен отвечать за одного и только одного актора». Под модулем в данном случае понимается файл с исходным кодом.

В качестве примера нарушения принципа можно привести такой код:

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

При применении принципа единственной ответственности должен получиться примерно такой код:

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

Альтернативная трактовка

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

Класс PersonRepository одновременно реализует функциональность для взаимодействия с СУБД и кэшем, что является нарушением альтернативной трактовки принципа единственной ответственности, да и в целом плохой практикой. Вместо этого будет лучше выделить логику кэширования в отдельный класс и воспользоваться шаблоном проектирования «Адаптер», например. В итоге должен получиться такой набор классов:

Теперь JdbcTemplatePersonRepository отвечает только за работу с СУБД, а CachingPersonRepository — за работу с кэшем.

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

Совмещение каноничной и альтернативной трактовок

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

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

Функциональный дизайн

Нередко дальнейшим развитием альтернативной трактовки становится атомизация кода, когда каждый интерфейс объявляет лишь один метод, являясь таким образом функциональным интерфейсом, а реализующие его классы реализуют либо только его, либо ещё один интерфейс (шаблон проектирования «Адаптер»). Такой подход действительно существует, но называется он «Функциональный дизайн» или «Шаблон функционального дизайна». В действительности с принципом единственной ответственности его сложно связать, хотя применение функционального дизайна де-факто выполняет требования принципа единственной ответственности.

В качестве примера можно привести следующий код:

В результате класс FindPersonByIdMappingSqlQuery получился очень компактным и удобным для понимания и тестирования. Если уж и связывать функциональный дизайн с принципами SOLID, то ближе всего к нему будет принцип разделения интерфейсов (Interface Segregation Principle; ISP), о котором я расскажу в одной из следующих статей.

Масштабирование принципа

Перечитывая книгу «Чистая Архитектура» в очередной раз, я поймал себя на мысли, что более точные формулировки принципа единственной ответственности в какой-то мере напоминают мне Закон Конвея, который звучит следующим образом: «Любая организация, которая разрабатывает систему (в широком смысле), вынуждена создавать проекты, структуры которых являются копией структуры связей организации». Да, так получилось, что формулировку Закона Конвея я узнал уже после ознакомления с принципами SOLID и прочтения книги Роберта Мартина, из-за чего при первом её прочтении не заметил сходств.

Если отталкиваться от Закона Конвея и принципов разделения кода на ограниченные контексты, применяемые в предметно-ориентированном проектировании (Domain Driven Design; DDD), то можно применять принцип единственной ответственности не к отдельным классам и интерфейсам, а к их группам (компонентам). И в этом случае принцип единственной ответственности превращается в принцип согласованного изменения (Common Clojure Principle; CCP).