Принцип единой ответственности или принцип единственной обязанности (Single Responsibility Principle)

Если искать в интернете определение принципа единой ответственности SOLID (SPR: Single Responsibility Principle), можно наткнуться на множество статей, которые почему-то описывают принцип единой обязанности (на английском он тоже звучит как Single Responsibility Principle), называя его принципом SOLID коим он не является. Некоторые статьи и видео все же описывают в примерах принцип единой ответственности, но приводят в качестве аргументов необходимости его применения принцип единой обязанности. В общем, я не нашел четкого разделения принципа единой обязанности и принципа единой ответственности и решил изложить свои соображения по этому поводу в этой статье, чтобы с этим разобраться. 

Проблема терминологии принципа единой ответственности

В своей книге “Чистый код. Создание, анализ и рефакторинг” согласно алфавитному указателю Роберт Мартин упоминает принцип единой ответственности (Single Responsibility Principle (SRP)). 

В первой главе присутствуют слова Берна Страуступа (создателя С++) по поводу того, что такое чистый код:

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

Забегая наперед, сразу скажу, что эта формулировка больше похожа на принцип единой обязанности и ничего общего с принципом единой ответственности не имеет. Почему так будет понятнее позже.

Более подробно Роберт Мартин останавливается на Single Responsibility Principle (в русском переводе почему-то принципе единой обязанности) в своей книге “Принципы, паттерны и методики гибкой разработки на C#”. 

В книге этот принцип описан так: 

У класса должна быть только одна причина для изменения.

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

Роберт Мартин в русском переводе пишет, что в контексте принципа Single Responsibility Principle (SRP) мы будем называть обязанностью причину изменения. 

С точки зрения русского языка эта фраза звучит довольно странно. Причиной изменения более правильно называть именно ответственность компонента перед внешними требованиями, а выполнение взятых на себя обязательств в своей зоне ответственности — обязанностью метода.

В книге “Чистая архитектура. Искусство разработки программного обеспечения” Роберт Мартин в третьей главе, которая называется “Принципы дизайна” пишет следующее.

Принципы SOLID определяют, как объединять функции и структуры данных в классы и как эти классы должны сочетаться друг с другом.

SRP: Single Responsibility Principle — принцип единой ответственности является следствием закона Конвея, который формулируется следующим образом. Лучшей является такая структура программной системы, которая формируется в основном под влиянием социальной структуры организации, использующей эту систему, поэтому каждый программный модуль имеет одну и только одну причину для изменения (ответственность).

Сам Роберт Мартин указывает на то, что из всех принципов SOLID принцип единой ответственности (Single Responsibility Principle) является наиболее трудно понимаемым. И допускает, что это вероятнее всего обусловлено выбором названия, недостаточно точно соответствующего сути принципа. Услышав это название, многие программисты решают, что каждый модуль должен отвечать за что-то одно.

Такой принцип действительно существует. Он гласит: 

Функция должна делать что-то одно и только одно. 

На русском этот принцип скорее всего правильно называть “Принцип единой обязанности”, что на английском звучит как “Single Responsibility Principle”, и обычно вводит всех в заблуждение. Этот принцип используется для деления функций на более мелкие на более низком уровне. Но он не является одним из принципов SOLID, сформулированных Робертом Мартином и тем более не является принципом единой ответственности SOLID.

Традиционно принцип единой ответственности описывается так: 

Модуль должен иметь одну и только одну причину для изменения.

Если рассматривать этот принцип с точки зрения удовлетворения бизнес требований заказчика, то можно его перефразировать так: 

Модуль должен отвечать за требования одного и только одного стекхолдера бизнес требований заказчика. 

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

Зачем нужен принцип единой ответственности

Зачем же придерживаться принципа единой ответственности и почему принцип единой обязанности и принцип единой ответственности — это все же разные принципы?

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

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

Рассмотрим пример, предлагаемый Робертом Мартином в книге “Чистая архитектура. Искусство разработки программного обеспечения”, но в разрезе сбора бизнес требований и более упрощенный, чем оригинал, приведенный в книге.

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

После сбора требований со всех стекхолдеров бизнеса оказалось, что всем троим подходит один вариант отчета. В итоге, принимается решение сделать один компонент, реализующий один вариант отчета.

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

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

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

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

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

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

 

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