Pull to refresh

SOLID: S — Single-Responsibility Principle

Level of difficultyEasy

История первого принципа из SOLID берёт своё начало ещё в 1972 году, когда Дэвид Лордж Парнас, канадский пионер в области программной инженерии, выпустил статью под названием «On the Criteria To Be Used in Decomposing Systems into Modules». Она появилась в декабрьском выпуске журнала «Communications of the ACM», том 15, номер 12. В своей статье Парнас сравнивал две разные стратегии декомпозиции и разделения логики на примере простого алгоритма. Вывод статьи заключался в следующем:

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

(«On the Criteria To Be Used in Decomposing Systems into Modules», D.L. Parnas)

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

Также, два года спустя, 30 августа 1974 года, Эдсгер Вибе Дейкстра написал свою работу «On the role of scientific thought», введя такой термин как Separation of Concerns:

Позвольте мне попытаться объяснить, что, на мой взгляд, характерно для любого интеллектуального мышления. Это готовность глубоко изучить определённый аспект предмета в изоляции ради его внутренней согласованности, при этом осознавая, что вы занимаетесь только одним из аспектов. Мы знаем, что программа должна быть корректной, и можем изучать её только с этой точки зрения; мы также знаем, что она должна быть эффективной, и можем рассматривать её эффективность в другой день, так сказать. В другом настроении мы можем задаться вопросом, желательна ли программа и, если да, то почему. Но ничего не будет достигнуто — наоборот! — если рассматривать эти различные аспекты одновременно. Это то, что я иногда называю «разделением забот» (separation of concerns), которое, даже если его невозможно реализовать в совершенстве, всё же остаётся единственным доступным методом для эффективного упорядочивания мыслей, который мне известен. Это то, что я имею в виду под «фокусировкой внимания на каком-либо аспекте»: это не означает игнорирование других аспектов, а лишь признание того, что с точки зрения одного аспекта другие не имеют значения. Это умение быть одновременно сосредоточенным на одном аспекте и многозадачным.

(«On the role of scientific thought», Edsger W. Dijkstra)

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

Бурное развитие принципов архитектуры

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

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

Именно в этот период были введены и активно развивались два фундаментальных понятия — Coupling (зацепление) и Cohesion (связанность). Эти термины были впервые предложены Ларри Константином и затем развиты Томом ДеМарко в его книге «Structured Analysis and System Specification», Мейлиром Пейдж-Джонсом в «Practical Guide to Structured Systems Design» и другими инженерами:

  1. Coupling: описывает степень зависимости между различными модулями системы. Чем ниже связность, тем более независимыми и изолированными друг от друга являются модули. Низкая связность повышает модульность системы, делает её более устойчивой к изменениям и упрощает тестирование.

  2. Cohesion: отражает степень связанности элементов внутри одного модуля. Высокая сцепленность означает, что все элементы модуля работают вместе для выполнения одной чёткой задачи. Модули с высокой сцепленностью проще понимать, сопровождать и тестировать.

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

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

Single-Responsibility Principle

Роберт Мартин, в конце 1990-х годов попытался объединить понятия, описанные выше в принцип, который он назвал: «Принцип единой ответственности». Он гласит:

«У каждого программного модуля должна быть одна и только одна причина для изменений.»

Однако, что определяет причину для изменения? Можно ли считать исправление ошибки или рефакторинг причиной для изменения? Для того чтобы это понять, нужно провести связь между понятиями «причина для изменения» и «ответственность».

Код не несёт ответственности за ошибки или рефакторинг. Это ответственность программиста, а не программы. Но за что тогда отвечает программа? Лучше спросить: перед кем отвечает программа? Ещё лучше: кому должен отвечать дизайн программы?

Представьте себе компанию, в которой есть стандартные должности: технический, финансовый и операционный директора. Представьте также себе класс, в котором находятся следующие методы: save(), calculatePay() reportByHours().

Кого бы уволил генеральный директор компании, если бы метод save() повредил базу данных с клиентами, calculatePay() неправильно рассчитывал зарплату в течение нескольких лет, а reportByHours() выдавал в течение года неправильный отчёт, в результате чего компания понесла убыток в несколько миллионов долларов?

Вполне очевидно, что если бы метод save() повредил базу данных, то за это был бы ответственен технический директор. Если бы calculatePay() неправильно рассчитывал зарплату, то был бы ответственен финансовый директор. А если бы reportByHours() выдавал неверный отчёт, то ответственность легла бы на операционного директора.

Это и подводит нас к сути принципа единой ответственности: этот принцип касается людей. Когда вы пишите какой-то модуль, вы хотите быть уверенными, что требования по изменению этого модуля будут исходить только от одного человека или отдела, представляющих какую-то одну бизнес-функцию. Почему? Потому что мы не хотим, чтобы изменения, запрошенные операционным отделом в reportByHours() сломали метод calculatePay(), который относится к финансовому отделу.

Вот почему мы не помещаем SQL в шаблоны страниц, не генерируем HTML методах, которые вычисляют результаты. Вот почему бизнес-правила не должны знать схему базы данных.

«Соберите вместе вещи, которые изменяются по одним и тем же причинам.Разделите те вещи, которые изменяются по разным причинам.»

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

Хочу обратить внимание, что принцип SRP очень близок к GRASP паттернам, таким как Information expert и Protected variations.

Примеры нарушений Single-Responsibility Principle

Один из самых часто встречающихся нарушений принципа единой ответственности — это антипаттерн God Object. В таком классе находится всё что угодно. Всё, для чего было лень программисту вводить отдельные концепции, смысловые единицы. Проблема этих классов в том, что через них проходят если не все, то большинство осей изменений приложения. Стоит заметить, что это и нарушение Information expert из GRASP. Какое бы требование не пришло от бизнеса, какой бы запрос на изменение вы ни получили, скорее всего, вам придётся что-то изменить в этом классе.

Также, нарушением SRP можно считать полюбившийся многим enterprise паттерн ActiveRecord, описанный Мартином Фаулером в книге «Patterns of Enterprise Application Architecture», т. к. через такие классы проходит как минимум две оси изменений: одна относится к бизнесу, а другая к персистенции. А также Separation of Concerns, из-за сильной взаимосвязи логики приложения и базы данных. Тем не менее ActiveRecord может быть подходящим решением для небольших приложений, где простота реализации перевешивает недостатки в виде слияния бизнес-логики и логики доступа к данным. Такие фреймворки и библиотеки как Ruby on Rails (Ruby), Eloquent и Yii (PHP), Peewee (Python) и Sequelize (JavaScript) используют этот паттерн.

Заключение

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

Однако важно помнить: за изменениями всегда стоят люди. Именно они инициируют запросы на изменение. Если ваш код объединяет элементы, которые интересуют разные группы людей по совершенно разным причинам, это создаёт путаницу и усложняет взаимодействие как для вас, так и для них.

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

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.