Найти в Дзене
Геннадий Шушпанов

О единственной ответственности

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

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

Реальные объекты

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

Нам не жить друг без друга

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

-2

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

-3

Теперь видно, что методы Save и Load несут не какую-то отстраненную ответственность, а сохраняют и восстанавливают состояние класса. Вынесите их в отдельный класс, как рекомендуют, и вам понадобится сделать публичным поле content, а инкапсуляция помашет ручкой на прощание. Да, редактирование и персистентность разные ответственности и имеют разные причины для изменений, но они используют один и тот же элемент состояния класса. И это их роднит больше, чем разделяет.

Неудачной является и попытка показать последствия несоблюдения SRP. В данном примере предлагают добавить как наследника не персистентный документ. Ему не нужны методы Save/Load, а они есть, и эта проблема объявляется следствием несоблюдения SRP. Но она не связана с SRP. Новые требования ломают существующую модель, которая предполагала наличие только персистентных документов. Помимо этого желание добавить новый класс как наследника противоречит LSP, поскольку его поведение существенно отличается от поведения базового класса. И это не лечится сменой ролей родитель/наследник. Тут надо модель чинить, а не ответственности делить.

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

-4

Границы и иерархии

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

  • Летательный аппарат (ЛА). Инкапсулирует аэродинамическое движение.
  • Управляемый ЛА. Добавлена система управления полетом, позволяющая выбрать один способов полета: например, движение по маршруту или один из методов наведения.
  • ЛА с двигателем. ЛА получает двигатель с тягой и топливную систему, определяющую логику расхода топлива из нескольких баков. Здесь у нас опять не единственная ответственность из-за тесной связи двигателя и топливной системы с параметром состояния -- расходом топлива.
  • Носитель. Добавляем систему управления грузами. ЛА получает возможность нести оружие и подвесные топливные баки.
  • Истребитель. Оснащен радаром и системой управления оружием. Вновь две тесно связанные системы.

Вернемся к документам. У нас были только персистентные, а теперь есть и не персистентные. Здесь возможны следующие варианты.

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

Документы редактируются одинаково. В этом случае можно либо выстроить иерархию "размазав" работу с контентом по классам и ослабив инкапсуляцию (protected content), рассматривая персистентный документ как расширение не персистентного. Либо сохранить инкапсуляцию, наплевав на повторное использование кода и сделав два отдельных класса. Выделение контента в отдельный класс не поможет, вы просто перенесете проблему с одного места на другое, но уже с потерей инкапсуляции по контенту.

SRP не волшебная палочка. Его использование не гарантирует, что все будет хорошо. Хорошо будет, если вы будете много думать, приводя ответственности и состояния к гармонии. Ну а одна там ответственность или две -- кто вас осудит, если все будет отлично работать.