Принцип единственной ответственности сложно соблюдать, но почему? Давайте углубимся в это.

Введение

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

SOLID — полезная аббревиатура, которую вы можете использовать, чтобы запомнить пять основных принципов:

Когда и где мне следует использовать принципы SOLID?

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

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

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

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

В этой статье давайте начнем с первого принципа SOLID, принципа единой ответственности (SRP).

Что такое принцип единой ответственности?

Роберт С. Мартин (дядя Боб) определяет принцип единственной ответственности следующим образом:

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

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

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

Хорошо, во-первых, вы можете думать о модуле как о функции, классе, компоненте или даже микросервисе.

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

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

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

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

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

Как видите, у класса Auth есть метод login, отвечающий за его бизнес-логику и отправку SMS пользователю после входа в систему. Класс Auth делегирует отправку задачи SMS другому классу SmsProvider, который инкапсулирует ее реализацию таким образом, что класс Auth не знает, как SmsProvider реализует свою реализацию.

Что такое ответственность? А как насчет причины для изменения?

Ответственность – это ответ на вопрос о том, как что-то делается.

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

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

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

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

В какой степени мы должны применять SRP?

К сожалению, следование SRP звучит проще, чем есть на самом деле.

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

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

Вы должны определить точку баланса между чрезмерной простотой и чрезмерной сложностью вашего кода. Эта точка может быть определена:

  • Спросите себя: «За что отвечает этот модуль?». Если в вашем ответе есть слово «и», вы, вероятно, нарушаете SRP.
  • Спросите себя: «Каковы возможные причины для изменения этого модуля?». Если у вас много причин, не связанных друг с другом, вы, скорее всего, нарушаете SRP, а ваш модуль имеет низкую связность и тесно связан.

Связь между SRP и сцеплением, сплоченностью и разделением интересов

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

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

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

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

Еще одна концепция, тесно связанная с SRP, — это сплоченность. Сплоченность относится к тому, насколько сильны отношения между элементами модуля. Чем больше обязанностей у модуля, тем ниже связь между его элементами.

Взгляните на этот пример, чтобы лучше понять эти принципы, Class имеет три поля и два метода. method1 использует только field2 и не использует другие поля. method2 использует field1, а field3 не использует field2.

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

Теперь мы можем сказать, что Class1 и Class2 очень связаны, слабо связаны, а задачи идеально разделены.

Почему мы должны применять SRP?

Теперь мы знаем, где и когда мы должны использовать SRP, но почему мы должны утруждать себя его применением?

Применение SRP дает множество преимуществ:

  1. Мы знаем, что требования со временем меняются. Каждое изменение затрагивает ответственность как минимум одного класса. Чем больше обязанностей у вашего класса, тем больше у вас причин для изменений.
  2. Одноцелевой модуль гораздо легче читать, объяснять и понимать. Возможно, вы помните, какое разочарование вы чувствовали, когда вам приходилось реорганизовывать большой класс.
  3. Конечно, отдельные модули более гибкие и настраиваемые, чем многие ответственные модули. Если у вас большой класс и вы хотите добавить новую функцию или сделать функцию настраиваемой, да, вы можете сделать это в самом классе, но только за счет увеличения размера и сложности класса.
  4. Отдельные ответственные модули, вероятно, более пригодны для повторного использования, чем многие ответственные модули. Вы можете заметить, что методы только с одной целью, скорее всего, не имеют побочных эффектов и не зависят от состояния класса. Да, вы правы, как вы думали. Это функциональное программирование. SRP подталкивает нас к функциональному стилю программирования.
  5. Легче тестировать и поддерживать одноцелевые модули.
  6. Наличие классов и методов только с одной целью помогает легко исследовать проблемы с производительностью. И что еще более примечательно, на уровне распределенных систем сервисы, имеющие только одну цель, легче отслеживать нагрузку и узкие места в ресурсах и, как следствие, независимо масштабироваться вверх или вниз.
  7. Если ваш класс зависит от одного или нескольких классов, любое изменение в этом классе повлияет на его зависимости. Вам может потребоваться обновить эти зависимости или перекомпилировать их, даже если они не затронуты вашим изменением напрямую.

Я уверен, что буду использовать SRP всегда и везде

Прежде чем зайти так далеко, имейте в виду следующие моменты:

  1. На уровне распределенных систем чем больше сервисов вы создаете, тем ниже надежность. Да, есть много способов преодолеть этот момент, но вы должны помнить, что все имеет свою цену времени, усилий и денег.
  2. Как мы знаем, разделение задач увеличивает размер кода, а также усилия и время, необходимые для написания этого кода. Однако в долгосрочной перспективе это уменьшает усилия и время.
  3. Действительно, разделение задач в нашем коде влияет на общую производительность. Впрочем, на уровне кода этим моментом можно было бы пренебречь, а вот на уровне распределенных систем он сильно бьет. Многие сервисы напрямую влияют на производительность системы из-за более высокой задержки и проблем с сетью.

Давайте применим SRP к примеру

Давайте представим простой пример, в котором представлен класс с множеством причин для изменения, а затем попробуем применить к нему SRP.

Прежде всего, давайте проведем наше исследование и спросим себя:

  • Какова ответственность этого класса? Вы можете сказать, что он только регистрирует пользователя. Другой может пойти глубже и сказать, что он отвечает за ведение журнала, проверку и сохранение. Хорошо, если вы запутались с идентификацией, переходите к следующему тесту.
  • Каковы возможные причины изменения этого класса? Я думаю, мы согласны с тем, что мы можем изменить механизм ведения журнала, критерии проверки или подход к сохранению, верно? Так что у нас есть много причин изменить этот класс. Поэтому может быть полезно провести рефакторинг вашего класса и применить SRP.

Как результат. Давайте рефакторим этот пример с учетом SRP.

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

Заключение

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

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

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

Большое спасибо, что оставались со мной до сих пор. Надеюсь, вам понравится читать эту статью.

Если вы нашли эту статью полезной, ознакомьтесь также с этими статьями:

Ресурсы

Первоначально опубликовано на https://mayallo.com.