Принципы SOLID и поддерживаемый код

Если вы какое-то время занимались программным обеспечением, то почти наверняка слышали о принципах SOLID. Короче говоря, это набор принципов, призванных помочь разработчикам писать чистый, хорошо структурированный и легко обслуживаемый код. В программном обеспечении, как и в любом интеллектуальном и творческом начинании, ведется немало споров о «правильном» способе работы. У разных практикующих разные представления о том, что «правильно», в зависимости от их личного опыта и склонностей. Однако идеи, предложенные приверженцами SOLID, получили широкое распространение в сообществе разработчиков программного обеспечения, и согласны с ними или нет, они представляют собой полезный набор принципов, из которых можно извлечь лучшие практики. Более того, SOLID был полностью интегрирован в более широкий набор практик гибкой разработки, и поэтому понимание их является виртуальным требованием в современной индустрии программного обеспечения.

Разработчики и блоггеры писали о SOLID до бесконечности в разных местах сети. Изучая эту статью, я столкнулся с множеством таких ресурсов, некоторые из которых цитируются в конце статьи для справки. Итак, если принципы SOLID хорошо освещены в другом месте, зачем писать о них еще одну статью? Короче, в свое назидание. Написание сложных тем - один из лучших способов выучить их самостоятельно. По этой причине я планирую серию из пяти статей - по каждой из принципов SOLID. Далее следует первая такая статья, в которой основное внимание уделяется принципу единой ответственности. Хотя я не ожидаю, что мое добавление к корпусу по этой теме будет особенно уникальным, я надеюсь, что оно окажется полезным для некоторых читателей. И с этим, давайте погрузимся.

Краткое руководство по SOLID

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

  1. Принцип единой ответственности - классы должны иметь единственную ответственность и, следовательно, только единственную причину для изменения.
  2. O pen / Принцип закрытости - классы и другие сущности должны быть открыты для расширения, но закрыты для модификации.
  3. Принцип подстановки л исков - Объекты должны заменяться их подтипами.
  4. Принцип разделения интерфейсов I - интерфейсы должны быть специфичными для клиента, а не общими.
  5. Принцип инверсии зависимости D - полагаться на абстракции, а не на конкреции.

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

Принцип единой ответственности (SRP) гласит, что каждый класс или модуль в программе должен нести ответственность только за одну часть функциональности этой программы. Кроме того, элементы этой ответственности должны быть инкапсулированы ответственным классом, а не распределяться по несвязанным классам. Разработчик и главный проповедник SRP Роберт К. Мартин описывает ответственность как «причину для изменений». Итак, другой способ сформулировать SRP - это сказать, как это делает Мартин, что «у класса должна быть только одна причина для изменения».

Прежде чем идти дальше, стоит взглянуть на историю SRP. Первоначально Мартин ввел этот термин в свои Принципы объектно-ориентированного дизайна [1]. По словам Мартина, SRP берет свое начало в идее Тома Демарко о сплоченности, которая описывает степень, в которой элементы в данном классе / модуле связаны и релевантны друг другу. Кроме того, он основан на описании Дэвидом Парнасом инкапсуляции или сокрытия информации, в котором говорится, что атрибуты и поведение, относящиеся к данному объекту, должны быть объединены вместе и скрыты от внешнего доступа. [2]. Взятые вместе, эти идеи естественным образом приводят к тому принципу, что данная часть функциональности программного обеспечения (также известная как ответственность) должна быть объединена в один класс и скрыта от других элементов программы, открывая только те части, которые необходимы для функциональности программы. в целом.

На первый взгляд это кажется относительно простым. Отдельные части функциональных возможностей программы должны быть распределены между отдельными объектами, которые могут обрабатывать их без посторонней помощи. Но как определить «отдельный фрагмент» программы? Что такое «ответственность» и как вы оцениваете ее с точки зрения бизнеса? Мартин, широко известный как «дядя Боб», разъяснил именно эту проблему в статье в блоге 2014 года, где он привязал «ответственность» к идее заинтересованных участников [3]. Статью Мартина стоит прочитать, но, резюмируя, он утверждает, что если у части программного обеспечения есть несколько разных типов пользователей (актеров), то несопоставимые интересы каждого из этих пользователей определяют часть ответственности этого программного обеспечения. Мартин использует пример руководителей C-Suite (COO, CTO, CFO), каждый из которых использует какое-либо программное обеспечение для бизнеса по разным причинам. Более того, при рассмотрении того, как следует изменить программное обеспечение, каждый из этих участников должен иметь возможность диктовать изменения в программном обеспечении, не затрагивая интересы других участников.

«Объект Бога»

Как правило, лучший способ узнать о SRP - это увидеть его в действии. Но для этого нам, возможно, сначала следует посмотреть, как выглядит программа, когда она не придерживается SRP. Давайте взглянем на простую программу и посмотрим, сможем ли мы разделить ее обязанности. Далее следует краткая программа на Ruby, в которой описывается класс, описывающий поведение и атрибуты космических станций. Прочтите его и посмотрите, сможете ли вы определить: a) различные обязанности объектов, созданных классом SpaceStation; и б) типы участников, которые могут быть заинтересованы в деятельности космической станции.

По общему признанию, наши космические станции не особенно способны (я полагаю, что НАСА не приедет ко мне в ближайшее время); однако здесь еще есть что распаковать. Сразу видно, что у класса SpaceStation есть несколько разрозненных обязанностей. Грубо говоря, можно сказать, что работу космической станции можно разделить на четыре области: датчики; запасы; топливо; и двигатели. Хотя персонал не указан в классе, мы легко можем представить себе различных действующих лиц, которым могут быть небезразличны эти рабочие области. Возможно, ученый, который управляет датчиками, офицер по логистике, который занимается поставками, инженер, который управляет топливом, и пилот, который управляет двигателями. Учитывая такое разнообразие различных областей деятельности и заинтересованных участников, можем ли мы сказать, что этот класс нарушает SRP? Абсолютно.

В настоящее время наш класс SpaceStation является классическим примером так называемого «объекта Бога», то есть объекта, который знает обо всем и делает все. Это главный антишаблон в объектно-ориентированном программировании, и его следует избегать. Но почему? Что не так с «объектом Бога»? Ну, во-первых, такие объекты крайне сложно обслуживать. Наша программа сейчас очень проста, но представьте, что произошло бы, если бы мы добавили какие-то новые функции. Может быть, нашей космической станции потребуются помещения для экипажа, или медицинская зона, или отсек связи. Когда мы добавили такую ​​функциональность, класс SpaceStation вырастет до огромных размеров. Что еще хуже, каждая функциональность будет неразрывно связана со всеми остальными. Если мы захотим изменить способ управления топливным баком, мы можем случайно нарушить работу двигателя. Если ученый станции запросит изменения в работе сенсора, эти изменения могут повлиять на коммуникационный отсек.

Нарушение SRP может быть удобно вначале, но краткосрочные выгоды не окупаются затратами на долгосрочное обслуживание. Мы не только должны беспокоиться о том, как изменения в одном месте влияют на другое (из-за того, что мы не можем разделить проблемы), но и сам код становится громоздким и неприятным для работы. Гораздо лучший вариант - разбить функциональность программы на инкапсулированные части. Учитывая это, давайте внесем некоторые изменения в наш SpaceStation класс.

Разделение обязанностей

Ранее мы определили четыре приблизительных области работы, которыми управлял наш SpaceStation класс. Кажется, это хорошее место для начала, поскольку мы реорганизуем наш код, чтобы он больше соответствовал SRP.

Уф! Было много изменений, но все уже выглядит намного лучше. Теперь наш SpaceStation класс - это в основном просто контейнер для подчиненных частей, которые управляют отдельными операциями, а именно: задержка снабжения; комплект датчиков; топливный бак; и двигатели. Каждый из них принимает форму переменной экземпляра, которая устанавливается во время инициализации космической станции. Каждой переменной соответствует класс: Sensors; SupplyHold; FuelTank; и Thrusters.

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

Наши пользователи (научный сотрудник, пилот и т. Д.), Вероятно, сейчас достаточно довольны тем, как разбиты их соответствующие части, и они могут запрашивать изменения по мере необходимости; тем не менее, мы можем сделать еще больше. Обратите внимание, например, на метод report_supplies в классе SupplyHold и метод report_fuel в классе FuelTank. Что произойдет, если управление полетом на Земле потребует изменить способ представления отчетов? Что ж, нам придется изменить классы SupplyHold и FuelTank. Но что теперь, если авиадиспетчер решит изменить способ загрузки припасов или топлива на станцию? Хорошо, мы еще раз изменим соответствующие методы для этих классов. Хм… тогда может показаться, что у нас несколько причин для изменения именно этих классов. Мне это кажется нарушением SRP! Посмотрим, сможем ли мы внести еще несколько корректировок.

В этой последней версии нашей программы мы разделили обязанности по составлению отчетов на класс FuelReporter и класс SupplyReporter, оба из которых наследуются от родительского класса Reporter. Затем мы добавляем переменные экземпляра в наш класс SpaceStation, чтобы инициализировать соответствующие репортеры для его использования. Теперь, если управление полетом запрашивает изменения в процедурах отчетности, мы можем внести соответствующие изменения в Reporter подклассы, не затрагивая классы, по которым они сообщают.

Конечно, между нашими различными классами все еще существует некоторая взаимосвязь. Объект SupplyReporter зависит от получения объекта SupplyHold, как и объект FuelReporter зависит от объекта FuelTank. Обязательно, Thrusters тоже требует FuelTank, чтобы использовать его. Все это кажется мне разумным, поскольку некоторая связь неизбежна, и мы все еще можем изменять операции одного объекта, не оказывая существенного влияния на другие. Однако есть еще возможности для улучшения этой программы и предложения изменений, которые повысят гибкость и ремонтопригодность (и действительно, я приветствую такие предложения в комментариях!). На данный момент важно то, что эта версия кода является довольно значительным улучшением. над нашей первой версией «объекта Бога». Мы эффективно разделили обязанности на отдельные классы и, таким образом, снизили вероятность того, что изменения кода в одном месте нарушат работу в другом. Также гораздо приятнее работать, когда требуются обновления.

TL;DR

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

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

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

Ссылки

  1. Статья: SRP: Принцип единой ответственности (возражающий)
  2. Статья: О критериях разложения систем на модули (1971; Парнас)
  3. Статья: Принцип единой ответственности (20140508; Мартин, Р.)
  4. Статья: Секрет принципа единой ответственности (20170830)
  5. Статья: SOLID: Часть 1 - Принцип единой ответственности
  6. Статья: Понимание принципов SOLID: единственная ответственность
  7. Статья: Думаете, вы понимаете принцип единой ответственности?
  8. Статья: Принцип единой ответственности: рецепт отличного кодекса
  9. Википедия: принцип единой ответственности