Многоликий принцип единственности ответственности
June 21, 2021
Кажется, любой "солидный" программист знает что такое SOLID вообще и принцип единственности ответственности (SRP) в частности.
Спойлер, если вдруг не знаете
SOLID - это название принципов объектно-ориентированного дизайна, сформулированных Робертом Мартином, так же известным как анкл Боб. Принципы звучат следующим образом:
- Single Responsibility Principle: A module should have one, and only one, reason to change.
- Open/Closed Principle:: A software artifact should be open for extension but closed for modification.
- Liskov Substitution Principle:: What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T
- Interface Segregation Principle:: Clients should not be forced to depend upon interfaces that they do not use.
- Dependency Inversion Principle::
- High level modules should not depend upon low level modules. both should depend upon abstractions.
- Abstractions should not depend upon details. details should depend upon abstractions.
Когда речь заходит об SRP, я всегда уточняю, что именно имеет ввиду мой собеседник. Потому что у SRP существует как минимум пять разных формулировок и три интерпретации. И я не думаю, что подобная эмм… штуковина является хорошим руководством по разработке ПО.
Формулировки SRP
Для начала приведу неверную, но самую популярную формулировку:
Класс должен делать одну вещь
Которой вторит и русская википедия:
Для каждого класса должно быть определено единственное назначение. Все ресурсы, необходимые для его осуществления, должны быть инкапсулированы в этот класс и подчинены только этой задаче.
Хотя английская версия той же статьи приводит одну из формулировок Мартина.
Вообще, такой принцип есть - это один из принципов философии Unix. Но сам Мартин пишет в Чистой Архитектуре, что это не SRP:
It is too easy for programmers to hear the name and then assume that it means that every module should do just one thing.
Make no mistake, there is a principle like that. […] But it is not one of the SOLID principles — it is not the SRP.
Сам Мартин формулирует SRP тремя разными способами:
- Формулировка 2003 года
The Single Responsibility Principle (SRP) states that a class or module should have one, and only one, reason to change
- Формулировка 2014 года
Gather together the things that change for the same reasons. Separate those things that change for different reasons.
- Формулировка 2018 года
A module should be responsible to one, and only one, actor
Чтобы понять, что у них общего, мне потребовалось по три прочтения всех публикаций Мартина об SRP. Вы же сэкономите день-два жизни и получите ответ за пятнадцать минут, если дочитаете пост до конца.
Для начала рассмотрим как анкл Боб объясняет SRP последние двадцать лет.
Объяснения SRP
Agile software development Principles Patterns and Practices, 2003
Впервые SRP появился в Agile software development Principles Patterns and Practices. В этой книге Мартин пояснял его на примере разделения кода различных функций программы:
Consider the bowling game from Chapter 6. For most of its development the Game class was handling two separate responsibilities. It was keeping track of the current frame, and it was calculating the score. In the end, RCM and RSK separated these two responsibilities into two classes. The Game kept the responsibility to keep track of frames, and the Scorer got the responsibility to calculate the score. (see page 85.)
Clean Code, 2008
Пояснять SRP на примере разделения кода по функциям программы Мартин продолжает и в Clean Code:
public class SuperDashboard extends JFrame implements MetaDataUser {
public Component getLastFocusedComponent()
public void setLastFocused(Component lastFocused)
public int getMajorVersionNumber()
public int getMinorVersionNumber()
public int getBuildNumber();
}
The seemingly small SuperDashboard class in Listing 10-2 has two reasons to change. First, it tracks version information that would seemingly need to be updated every time the software gets shipped. Second, it manages Java Swing components (it is a derivative of JFrame, the Swing representation of a top-level GUI window)
The Clean Coder, 2011
Затем в The Clean Coder анкл Боб иллюстрирует SRP уже примером разделения аспектов реализации (пользовательского интерфейса и бизнес-правил):
There is a design principle called the Single Responsibility Principle (SRP). This principle states that you should separate those things that change for different reasons, and group together those things that change for the same reasons. GUIs are no exception.
The layout, format, and workflow of the GUI will change for aesthetic and efficiency reasons, but the underlying capability of the GUI will remain the same.
[...]
Design experts have been telling us for decades to separate our GUIs from our business rules.
The Single Responsibility Principle, 2014
В следующей публикации о SRP анкл Боб уже напрямую использует термин "разделение аспектов [реализации]" (separation of concerns):
Two years later, Edsger Dijkstra wrote another classic paper entitled On the role of scientific thought. in which he introduced the term: The Separation of Concerns. [...] This is the reason we do not put SQL in JSPs. This is the reason we do not generate HTML in the modules that compute results. This is the reason that business rules should not know the database schema. This is the reason we separate concerns.
Но здесь же, Мартин впервые объясняет SRP совсем в другом ключе:
And this gets to the crux of the Single Responsibility Principle. This principle is about people.
When you write a software module, you want to make sure that when changes are requested, those changes can only originate from a single person, or rather, a single tightly coupled group of people representing a single narrowly defined business function.
Здесь уже речь идёт о разделении кода по людям.
Clean Architecture, 2018
В окончательной же форме это объяснение появляется ещё через четыре года в Clean Architecture:
A module should be responsible to one, and only one, actor
Действующим лицом (actor) в этом случае является группа стейкходеров (людей так или иначе причастных к программе и её созданию) с одинаковыми потребностями.
Шестая формулировка SRP
Мне кажется более понятной шестая (уже моя) формулировка SRP:
Модуль должен отвечать за реализацию требований одного стейкхолдера.
Эта формулировка привязывается ко вполне определённому понятию - "требование". На мой взгляд, термин "требование" вызывает намного меньше разночтений, чем "действующее лицо". "Действующее лицо" - крайне неудачный термин, так как он чаще встречается в значении "пользователь программы", популяризированном UML-ем.
В моей формулировке понятно, почему надо разделять модули хранения данных и представления. Одни специфицируются человеком выполняющим роль администратора БД, а другие - человеком, выполняющим роль дизайнера интерфейсов. Также понятно и почему надо разделять модули, отвечающие за реализацию функций CFO и COO - требования к ним определяется разными людьми из разных отделов.
Но и с этой формулировкой есть пара проблем. Во-первых, инфраструктурные модули будут реализовывать минимум по два требования - функциональное и качественное. Во-вторых, я не видел проекта, где были бы внятно прописаны функциональные и нефункциональные требования, список стейкхолдеров и их привязка друг к другу.
Ни эта, ни любая другая формулировка SRP не даёт программистам практического руководства к действию в каждодневной работе. Может быть, будет полезнее разбить SRP на несколько практических рекомендаций? Например:
- формируйте направленный ациклический граф зависимостей между модулями;
- разделяйте ввод-вывод (в том числе GUI) и бизнес-правила;
- разделяйте код реализующий разные функции системы;
- пишите тесты. В тестах мокайте только внешние системы, а system under test создавайте "руками" (а не с помощью DI-контейнера).
Эти рекомендации не нуждаются большом предварительном проектировании с определением стейкхолдеров и требований. И следование им можно контролировать механически.
Применение таких рекомендаций в каждодневной работе доступно намного более широкому кругу программистов и также даст поддерживаемую и стабильную систему на выходе.
Помимо приведенных, есть ещё одна важная рекомендация: разделяйте "стандартную библиотеку" (домен) и "скрипты" (функции) приложения. Об этом пишут и Мартин в Clean Architecture - разделение сущностей и интеракторов, и Эванс в DDD - разделение сервисов приложения и сущностей и доменных сервисов. Но ни они, ни я не можем дать объективного критерия, по которому можно следить за соблюдением этой рекомендации механически. Поэтому я не включаю её в ряд простых и понятных.
На суку висит мочало, начинаем всё сначала
SOLID relevance, 2020
Постоянное изменение формулировок и интерпретаций SRP можно было бы объяснить эволюцией понимания SRP самим Мартином. Сначала для него SRP был о разделении по функциям программы. Потом он понял, что по аспектам реализации код тоже необходимо разделять. Наконец, анкл Боб обобщил их через разделение по требованиям различных действующих лиц.
Это было отличное объяснение развития событий. Пока Мартин не написал свой последний пост на тему SRP. В нём он снова откатился к разделению только по аспектам:
It is hard to imagine that this principle is not relevant in software. We do not mix business rules with GUI code. We do not mix SQL queries with communications protocols.
Если посмотреть на историю объяснений SRP с высоты "птичьего полёта", то становится видно что анкл Боб постоянно скачет между этими критериями декомпозиции кода:
Год | Источник | Критерий разделения |
---|---|---|
2003 | Agile software development Principles Patterns and Practices | Функциональность и намёк на действующее лицо* |
2008 | Clean Code | Функциональность |
2011 | The Clean Coder | Аспект реализации |
2014 | The Single Responsibility Principle | Функциональность, аспект реализации и действующее лицо |
2018 | Clean Architecture | Функциональность и действующее лицо, в меньшей степени аспект реализации** |
2020 | SOLID Relevance | Аспект реализации |
Разделение по функциональности и аспектам реализации программы - это не две разные точки зрения на один принцип. Это два разных принципа декомпозиции, которые ведут к разным результатам.
Разделение кода по функциональности != разделению кода по аспектам реализации
Можно разделять SQL и JSP и всё ещё использовать один и тот же код в разных функциях и ломать функции одних пользователей при модификации функций других пользователей.
И наоборот, можно завести по микросервису для каждого пользователя или функции системы и собрать в один метод и парсинг json-а, и бизнес-правила, и выполнение SQL-запросов. В итоге получить хрупкий код, который трудно поддерживать.
Аспекты реализации функциональности и сама функциональность - ортогональные оси декомпозиции кода.
SRP нарушает одну из собственных интерпретаций и смешивает различные аспекты дизайна программ. Зачем анкл Боб объединил два разных принципа в один - я не понимаю. Возможно, он сам не сразу понял, что код надо разделять и по функциональности, и по аспектам реализации, а когда понял - SOLID стал уже брендом, который было жаль терять.
Можем ли мы полагаться на принцип, формулировка и интерпретация которого меняется каждые три года? Я думаю нет.
Нам нужны новые принципы
В итоге мы приходим к тому, что:
- вообще, не очень понятно, что такое SRP. Разные разработчики понимают под этой аббревиатурой разные принципы дизайна. Даже сам Мартин постоянно по-разному формулирует и иллюстрирует SRP;
- то, что мне кажется Единственно Верной Версией SRP, невозможно использовать на практике. Потому что в реальной жизни необходимую для этой версии аналитику никто не делает.
- а если бы и делали, то SRP-идеал всё равно был бы недостижим. Так как у значительной части кода будет как минимум две причины для изменения - требования к функциональности и требования к способу реализации.
И хотя SRP является самым неоднозначным принципом SOLID-а, остальные четыре принципа тоже имеют разночтения и проблемы с применением на практике. Поэтому я не думаю, что SOLID в своём текущем виде является хорошим руководством по дизайну систем.
Тем не менее, в SOLID заключено много хороших и полезных идей, поэтому я не предлагаю упразднить его - я предлагаю его реставрировать. Актуальные идеи вычленить, уточнить и проиллюстрировать хорошими примерами. То, что потеряло свою значимость - убрать.
И главное - не загонять себя в рамки того, чтобы из первых букв новых принципов можно было составить красивый акроним.