Убедитесь, что у каждого класса есть только одна ответственность, почему?

37

Согласно документации Microsoft, статье SOLID Wikipedia SOLID или большинству ИТ-архитекторов мы должны гарантировать, что каждый класс несет только одну ответственность. Я хотел бы знать почему, потому что, если все, кажется, согласны с этим правилом, никто не соглашается с причинами этого правила.

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

Бастьен Вандамме
источник
1
У меня тоже был вопрос, который вы могли бы найти связанным: какова реальная ответственность класса?
Пьер Арло,
3
Разве все причины единоличной ответственности не могут быть правильными? Это делает обслуживание проще. Это облегчает тестирование. Это делает класс более устойчивым (в общем).
Мартин Йорк,
2
По той же причине у вас есть классы ВСЕ.
Давор Адрало
2
У меня всегда была проблема с этим утверждением. Очень сложно определить, что такое «единая ответственность». Отдельная ответственность может варьироваться от «проверить, что 1 + 1 = 2» до «вести точный журнал всех денег, внесенных в корпоративные банковские счета и снятых с них».
Дейв Най,

Ответы:

57

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

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

Doval
источник
12
+1 за упоминание функционального программирования как более безопасного способа составления абстракций.
logc
> но основные языки ООП относительно плохо склеивают функции. <Может быть, потому что языки ООП касаются не функций, а сообщений.
Джефф Хаббард
@JeffHubbard Я думаю, что вы имеете в виду, потому что они берут после C / C ++. В объектах нет ничего, что делает их взаимоисключающими с функциями. Черт, объект / интерфейс - это просто запись / структура функций. Притворяться, что функции не важны, неоправданно как в теории, так и на практике. Их нельзя избежать в любом случае - в конечном итоге вам придется разбрасывать «Runnables», «Фабрики», «Поставщики» и «Действия» или использовать так называемые «шаблоны» Стратегии и Шаблонного метода, и в конечном итоге куча ненужных шаблонов, чтобы выполнить ту же работу.
Доваль
@ Довал Нет, я действительно имею в виду, что ООП занимается сообщениями. Это не значит, что данный язык лучше или хуже функционального языка программирования при склеивании функций - просто это не главная задача языка .
Джефф Хаббард
По сути, если ваш язык предназначен для того, чтобы сделать ООП легче / лучше / быстрее / сильнее, то то, на что хорошо склеиваются функции - это не то, на чем вы сосредоточены.
Джефф Хаббард
30

Лучшее обслуживание, простое тестирование, быстрое исправление ошибок - это только (очень приятные) результаты применения SRP. Основная причина (по выражению Роберта С. Матина):

У класса должна быть одна и только одна причина для изменения.

Другими словами, СРП вызывает смену населенного пункта .

SRP также продвигает СУХОЙ код. Пока у нас есть классы, которые несут только одну ответственность, мы можем использовать их где угодно. Если у нас есть класс с двумя обязанностями, но нам нужен только один из них, а второй вмешивается, у нас есть 2 варианта:

  1. Скопируйте и вставьте класс в другого и, возможно, даже создайте еще одного мутанта с множеством обязанностей (немного увеличьте технический долг).
  2. Разделите класс и сделайте его таким, каким он должен быть в первую очередь, что может быть дорогостоящим из-за широкого использования исходного класса.
Мацей Халапук
источник
1
+1 Для привязки SRP к DRY.
Махди
21

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

Что касается того, который является правильным: все три из них. Все они являются преимуществами использования единой ответственности и причины, по которой вы должны ее использовать.

Что касается того, что они имеют в виду:

  • Лучшее обслуживание означает, что его легче менять, и оно меняется не так часто. Поскольку кода меньше, и этот код ориентирован на что-то конкретное, если вам нужно изменить что-то, не относящееся к классу, класс менять не нужно. Кроме того, когда вам нужно изменить класс, если вам не нужно менять публичный интерфейс, вам нужно только беспокоиться об этом классе и ничего больше.
  • Простое тестирование означает меньше тестов, меньше настроек. В классе не так много движущихся частей, поэтому число возможных сбоев в классе меньше, и, следовательно, меньше случаев, которые необходимо проверить. Будет меньше настраиваемых приватных полей / участников.
  • Из-за двух вышеперечисленных классов вы получаете класс, который меньше меняется и менее терпит неудачу, и, следовательно, более устойчивый.

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

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

Миямото Акира
источник
Я не думаю, что высказанные здесь соображения являются оправданными. В частности, как избежать игр с нулевой суммой, когда упрощение класса приводит к усложнению других классов без какого-либо влияния на кодовую базу в целом? Я верю, что ответ @ Doval решает эту проблему.
2
Вы делаете то же самое для всех классов. Вы заканчиваете большим количеством классов, все соблюдают SRP. Это делает структуру более сложной с точки зрения связей. Но простота в каждом классе оправдывает это. Мне нравится ответ @ Doval, хотя.
Миямото Акира
Я думаю, что вы должны добавить это к своему ответу.
4

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

  • Лучшее обслуживание : в идеале, всякий раз, когда функциональность системы должна меняться, должен быть один и только один класс, который должен быть изменен. Четкое соответствие между классами и обязанностями означает, что любой разработчик, участвующий в проекте, может определить, к какому классу это относится. (Как отметил @ MaciejChałapuk, см. Роберт С. Мартин, «Чистый код» .)

  • Более простое тестирование : в идеале классы должны иметь как можно меньший открытый интерфейс, а тесты должны затрагивать только этот открытый интерфейс. Если вы не можете проверить с ясностью, потому что многие части вашего класса являются частными, то это явный признак того, что у вашего класса слишком много обязанностей, и что вы должны разделить его на более мелкие подклассы. Обратите внимание, что это относится также к языкам, где нет «публичных» или «частных» учеников; наличие небольшого общедоступного интерфейса означает, что клиентскому коду очень ясно, какие части класса предполагается использовать. (См. Кент Бек, «Разработка через тестирование» для более подробной информации.)

  • Надежный код : ваш код не будет отказывать более или менее часто, потому что он хорошо написан; но, как и весь код, его конечная цель не в том, чтобы общаться с машиной , а с коллегами-разработчиками (см. Кент Бек, «Шаблоны реализации» , Глава 1.) Четкая база кода легче рассуждать, поэтому будет меньше ошибок. и между обнаружением ошибки и ее устранением будет меньше времени.

logc
источник
Если что-то должно измениться по деловым причинам, поверьте мне с SRP, вам придется изменить более одного класса. Сказать, что если изменение класса происходит только по одной причине, это не то же самое, что если изменение произойдет, это повлияет только на один класс.
Бастьен Вандамм
@ B413: Я не имел в виду, что любое изменение будет означать одно изменение в одном классе. Многие части системы могут нуждаться в изменениях, чтобы соответствовать одному бизнес-изменению. Но, возможно, у вас есть смысл, и я должен был написать «всякий раз, когда функциональность системы должна измениться».
Журнал
3

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

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

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

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

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

BillThor
источник
2

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

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

CodeART
источник
1

Я следую за мыслью 1 Class = 1 Job.

Использование физиологической аналогии: мотор (нейронная система), дыхание (легкие), пищеварительный (желудок), обонятельный (наблюдение) и т. Д. Каждый из них будет иметь подмножество контроллеров, но каждый из них будет иметь только 1 ответственность, независимо от того, нужно ли им управлять как работает каждая из их соответствующих подсистем или являются ли они подсистемой конечной точки и выполняют только одну задачу, такую ​​как поднятие пальца или наращивание волосяного фолликула.

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

Самая сложная часть того, что я испытал, это знание того, когда назначать класс в качестве оператора, супервизора или менеджера процесса. В любом случае вам необходимо соблюдать и обозначать его функциональность для 1 ответственности (Оператор, Супервайзер или Менеджер).

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

GoldBishop
источник
Даже если реализация обработки атмосферного кислорода в гемоглобин должна обрабатываться как Lungкласс, я бы сказал, что экземпляр Personдолжен все еще существовать Breathe, и, таким образом, Personкласс должен содержать достаточно логики, чтобы как минимум делегировать обязанности, связанные с этим методом. Кроме того, я бы предположил, что лес взаимосвязанных объектов, которые доступны только через общего владельца, часто легче рассуждать, чем лес, имеющий несколько независимых точек доступа.
суперкат
Да, в этом случае Легкое - Менеджер, хотя Вилли будет Супервайзером, а процесс Дыхания будет рабочим классом для передачи частиц Воздуха в Кровоток.
GoldBishop
@GoldBishop: Возможно, правильной точкой зрения было бы сказать, что Personкласс имеет в своей работе делегирование всей функциональности, связанной с чудовищно сложным объектом «реальный мир-человек», включая объединение его различных частей. , Заставить сущность делать 500 вещей - это уродливо, но если так работает моделируемая реальная система, иметь класс, который делегирует 500 функций, сохраняя при этом одну идентичность, может быть лучше, чем делать все с непересекающимися частями.
суперкат
@supercat Или вы можете просто разделить все на части и получить 1 класс с необходимыми супервизорами над подпроцессами. Таким образом, вы могли бы (теоретически) отделить каждый процесс от базового класса, Personно при этом сообщать о цепочке в случае успеха / неудачи, но не обязательно влиять на другой. 500 функций (хотя и приведенных в качестве примера, было бы за бортом и не поддерживается), я стараюсь, чтобы этот тип функциональности был производным и модульным.
GoldBishop
@GoldBishop: Мне бы хотелось, чтобы был какой-то способ объявить «эфемерный» тип, такой, чтобы внешний код мог вызывать на него члены или передавать его в качестве параметра собственным методам типа, но никак иначе. С точки зрения организации кода, деление классов имеет смысл, и даже наличие внешнего кода вызывает методы на один уровень ниже (например, Fred.vision.lookAt(George)имеет смысл, но разрешение сказать, что код someEyes = Fred.vision; someTubes = Fred.digestionкажется странным, так как затеняет отношения между someEyesи someTubes.
суперкат
1

Особенно с таким важным принципом, как «Единая ответственность», я лично ожидал бы, что есть много причин, по которым люди принимают этот принцип.

Некоторые из этих причин могут быть:

  • Техническое обслуживание - SRP гарантирует, что изменение ответственности в одном классе не влияет на другие обязанности, что упрощает обслуживание. Это потому, что если у каждого класса есть только одна ответственность, изменения, внесенные в одну ответственность, изолированы от других обязанностей.
  • Тестирование. Если у класса есть одна обязанность, гораздо проще понять, как проверить эту ответственность. Если у класса есть несколько обязанностей, вы должны убедиться, что вы тестируете правильную и что на тест не влияют другие обязанности, которые выполняет класс.

Также обратите внимание, что SRP поставляется с набором принципов SOLID. Соблюдать SRP и игнорировать все остальное так же плохо, как и не выполнять SRP. Таким образом, вы не должны оценивать SRP отдельно, но в контексте всех принципов SOLID.

Euphoric
источник
... test is not affected by other responsibilities the class hasНе могли бы вы уточнить это?
Махди
1

Лучший способ понять важность этих принципов - это иметь потребность.

Когда я был начинающим программистом, я не задумывался о дизайне, на самом деле, я даже не знал, что существуют шаблоны проектирования. Поскольку мои программы росли, изменение одной вещи означало изменение многих других вещей. Было трудно отследить ошибки, код был огромным и повторяющимся. Не было особой иерархии объектов, вещи были повсюду. Добавление чего-то нового или удаление чего-то старого вызовет ошибки в других частях программы. Пойди разберись.

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

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

Однако, как и вы, я все еще не уверен в «простом тестировании», потому что у меня еще не было необходимости проходить юнит-тестирование.

harsimranb
источник
Шаблоны так или иначе связаны с SRP, но SRP не требуется при применении шаблонов проектирования. Можно использовать шаблоны и полностью игнорировать SRP. Мы используем шаблоны для решения нефункциональных требований, но использование шаблонов не требуется ни в одной программе. Принципы необходимы для того, чтобы сделать развитие менее болезненным и (ИМО) должно стать привычкой.
Мацей Халапук
Полностью с вами согласен. SRP в некоторой степени обеспечивает чистоту проектирования и иерархии объектов. Это хороший менталитет для разработчика, поскольку он заставляет разработчика начать думать с точки зрения объектов и того, как они должны быть. Лично это оказало очень хорошее влияние на мое развитие.
harsimranb
1

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

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

Я думаю, что это можно выразить одним словом: сфера. Обратите пристальное внимание на область действия артефактов: чем меньше будет область действия в любой конкретной точке приложения, тем лучше.

Расширенная сфера = больше сложности = больше возможностей для того, чтобы что-то пошло не так.

jmoreno
источник
1

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

У класса может быть больше одной ответственности без проблем, но вы скоро столкнетесь с проблемами со слишком сложными классами.

Однако наличие простого правила «только одна ответственность» просто помогает узнать, когда вам нужен новый класс .

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

Ян
источник