Как с помощью принципа единственной ответственности писать гибкий и модульный код
21К открытий22К показов
Если вы занимались разработкой ПО, вам наверняка знакома аббревиатура SOLID.
Это свод принципов, призванный помочь разработчикам писать чистый, хорошо структурированный и легко читаемый код. Программисты представляют себе по-разному «правильный» подход к написанию приложений — это больше зависит от их личного опыта и предпочтений. Однако идеи SOLID широко распространены среди разработчиков ПО, более того, они были интегрированы в Agile.
Расшифровка аббревиатуры:
- Single Responsibility Principle (SRP) — принцип единственной ответственности. Каждый класс выполняет только свои задачи.
- Open-Closed Principle (OCP) — принцип открытости/закрытости — программные сущности (классы, модули, функции и пр.) должны быть открыты для расширения, но закрыты для модификации.
- Liskov Substitution Principle (LSP) — принцип подстановки Барбары Лисков гласит: «наследующий класс должен дополнять, а не изменять базовый».
- Interface Segregation Principle (ISP) — принцип разделения интерфейса: «много интерфейсов, специально предназначенных для клиентов, лучше, чем один интерфейс общего назначения».
- Dependency Inversion Principle (DIP) — принцип инверсии зависимостей: «абстракции не должны зависеть от деталей — детали должны зависеть от абстракций».
В данном материале мы рассмотрим SRP — принцип единственной ответственности.
Немного предыстории
Роберт Мартин изначально ввёл термин в качестве составляющей своего труда «Принципы объектно-ориентированного проектирования». В основу SRP Мартина легла закономерность связности, описанная Томом Демарко и Мейлиром Пейдж-Джонсоном.
Кроме того, в разработке ПО есть два схожих понятия – инкапсуляция и сокрытие информации. SRP включает в себя также и эти два (или одно) понятия от Дэвида Парнаса, который обозначил их примерно так: «декомпозиция системы на модули не должна основываться на анализе блок-схем или потоков исполнения. Вместо этого, каждый модуль должен содержать внутри некоторое решение (design decision), предоставляя минимальное количество информации о нём своим клиентам».
Суть SRP
Суть SRP в одном предложении: «соберите всё, изменяемое по одной и той же причине, но разделите изменяемое по разным причинам».
Если разные люди работают с одной и той же программой, то изменение части, с которой взаимодействует один человек, не должно влиять на ту часть, с которой работает другой.
«Божественный объект»
Лучший способ изучить SRP — увидеть его в действии. Ниже показан пример программы на Ruby, не соответствующей принципу единственной ответственности. Код описывает поведение и атрибуты космической станции. Посмотрите на него и попробуйте определить:
- обязанности объектов, конкретизированные в классе
SpaceStation
; - виды лиц, которые могут быть заинтересованы в деятельности космической станции.
Видно, что класс SpaceStation
имеет несколько «функций» или «задач»:
- работа с сенсорами;
- использование расходных материалов;
- расход топлива;
- использование подруливающих двигателей.
Субъекты не указаны в коде, но можно предположить, что это:
- научный работник, управляющий сенсорами;
- специалист по материально-техническому обеспечению;
- лицо, ответственное за запасы топлива;
- пилот.
Программа полностью не соответствует принципам SRP, но в полной мере отражает суть «Божественного объекта» — основного анти-шаблона в объектно-ориентированном программировани. При таком подходе объект хранит слишком большое количество данных и содержит много методов, поэтому его роль становится «божественной» или всеобъемлющей. Вместо того, чтобы общаться друг с другом, объекты обращаются к всеобъемлющему, а так как на нём завязан весь проект (или его большая часть), то его обслуживание усложняется, увеличивая риск поломки существующей функциональности.
Обратимся к примеру с космической станцией. Представьте, если надо добавить медицинский отсек, а из-за этого произойдут какие-нибудь проблемы с топливным. Попробуйте представить, что для того, чтобы шагнуть, вам нужно выгнуть левую руку назад, повернуть голову вправо и нагнуться. Работает? Да. Терпимо? Возможно. А если нужно бежать? А если с вёдрами? То же самое и с проектом — он будет работать, но до определённого момента.
Нарушение SRP может быть выгодно в начале проекта, но это лишь краткосрочно. С ростом проекта будут увеличиваться финансовые и временны́е ресурсы для исправления существующих проблем. Влияющие друг на друга участки кода, его громоздкость и нечитаемость — главные проблемы, о которых надо подумать при игнорировании SRP.
Разбивка по обязанностям
Выше были определены 4 функции станции, которые управлялись классом SpaceStation
. Они и будут отправной точкой рефакторинга кода. Теперь программа чуть больше соответствует SRP.
Теперь класс SpaceStation
скорее служит контейнером, внутри которого выполняются операции для подчинённых частей:
- набора сенсоров;
- системы подачи расходных материалов;
- топливного бака;
- подруливающих двигателей.
Каждая из частей принимает форму поля, задаваемого при инициализации космической станции. Для каждой переменной есть соответствующий класс:
Sensors
(сенсоры);SupplyHold
(поставки расходных материалов);FuelTank
(топливный бак);Thrusters
(подруливающие двигатели).
В этой версии кода есть некоторые важные отличия от предыдущей, а именно: отдельные элементы функциональности не только инкапсулированы в собственные классы, но и организованы таким образом, чтобы быть предсказуемыми и последовательными. Идея заключается в группировке сходных по функциональности элементов, для следования принципу связности, и в изолировании данных таким образом, чтобы они были доступны только для соответствующих субъектов. Если понадобится изменить принцип работы системы поставки с хэш-структуры на массив, то это легко можно сделать, используя класс SupplyHold
. Таким образом другие модули не будут затронуты. Причём класс SpaceStation
даже не будет догадываться о действиях, производимых в модулях.
Заметьте, что сейчас в коде выше есть методы report_supplies
и report_fuel
, содержащиеся в классах SupplyHold
и FuelTank
. Что, если с Земли попросят изменить механизм загрузки отчётов? Придётся редактировать оба предыдущих класса. А если руководство решит изменить технологию доставки топлива и расходных материалов? Кажется, придётся повторно изменять те же классы. Похоже на нарушение SRP. Надо бы исправить.
В последней версии программы обязанности «модулей» были разбиты на два дочерних класса FuelReporter
и SupplyReporter
, объединённых под родительским классом Reporter
. Далее были добавлены экземплярные переменные к классу SpaceStation
, чтобы запустить соответствующий Reporter
. Если руководству с Земли понадобится ещё что-то изменить, то можно внести правки в подклассы, не влияя на работу объектов (классов), о которых они докладывают.
Конечно, до сих пор есть некоторая связь между разными классами. Например, SupplyReporter
зависит от SupplyHold
, так же зависим и FuelReporter
от FuelTank
. Кроме того, подруливающие двигатели тоже должны быть связаны с топливным баком. Все эти связи кажутся довольно логичными и на этом уровне уже можно изменять код одного объекта, не влияя на другой (либо влияя незначительно).
В итоге код программы стал более «модульным» и обязанности объектов ясным образом были обозначены. Вероятность «поломки» кода значительно уменьшена, а работать с ним стало приятнее, так как «божественный объект» (которым был весь код до второй редакции) был преобразован в SRP-код, если так можно выразиться.
21К открытий22К показов