Уточнить принцип единой ответственности

64

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

Как бы вы описали, что означает «одна вещь»? Каковы конкретные признаки того, что класс действительно делает больше, чем «одно»?

dsimcha
источник
6
+1 за сверх "код равиоли". В начале своей карьеры я был одним из тех людей, которые зашли слишком далеко. Не только с классами, но и с модульностью методов. Мой код был усыпан тоннами маленьких методов, которые делали что-то простое, просто для того, чтобы разбить проблему на маленькие кусочки, которые могли поместиться на экране без прокрутки. Очевидно, это часто заходило слишком далеко.
Бобби Столы

Ответы:

50

Мне очень нравится, как Роберт К. Мартин (дядя Боб) формулирует принцип единой ответственности (ссылка на PDF) :

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

Оно слегка отличается от традиционного определения «должен делать только одно», и мне это нравится, потому что оно заставляет вас изменить свой взгляд на свой класс. Вместо того, чтобы думать о том, «это делает что-то одно?», Вы вместо этого думаете о том, что может измениться и как эти изменения влияют на ваш класс. Так, например, если база данных изменяется, должен ли ваш класс меняться? Что делать, если изменяется устройство вывода (например, экран, или мобильное устройство, или принтер)? Если ваш класс должен измениться из-за изменений во многих других направлениях, это признак того, что у вашего класса слишком много обязанностей.

В связанной статье дядя Боб делает вывод:

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

Paddyslacker
источник
2
Мне нравится, как он это заявляет, кажется, что легче проверить, когда связано с изменением, чем абстрактную «ответственность».
Матье М.
Это на самом деле отличный способ выразить это. Я люблю это. Как примечание, я обычно склоняюсь к тому, что ПСП применяется гораздо сильнее к методам. Иногда классу просто нужно сделать две вещи (может быть, он связывает два домена), но метод почти никогда не должен делать больше, чем то, что вы можете кратко описать с помощью сигнатуры его типа.
CodexArcanum
1
Только что показал это моему Граду - отличное чтение и чертовски хорошее напоминание для себя.
Мартейн Вербург
1
Хорошо, это имеет смысл, когда вы комбинируете это с идеей, что не слишком сильный код должен планировать только те изменения, которые вероятны в обозримом будущем, а не каждое возможное изменение. Тогда я бы немного повторил это, поскольку «должна быть только одна причина для изменения класса, который может произойти в обозримом будущем». Это поощряет простоту в частях дизайна, которые вряд ли изменятся и разъединение в частях, которые могут измениться.
dsimcha
18

Я продолжаю спрашивать себя, какую проблему пытается решить SRP? Когда мне помогает SRP? Вот что я придумал:

Вы должны рефакторинг ответственности / функциональности вне класса, когда:

1) Вы дублировали функционал (СУХОЙ)

2) Вы обнаружите, что вашему коду необходим другой уровень абстракции, чтобы помочь вам понять его (KISS)

3) Вы обнаружите, что специалисты по предметной области понимают, что части функциональности являются частью другого компонента (вездесущий язык)

Вы НЕ ДОЛЖНЫ рефакторинг ответственности вне класса, когда:

1) Нет дублирующихся функций.

2) Функциональность не имеет смысла вне контекста вашего класса. Другими словами, ваш класс предоставляет контекст, в котором легче понять функциональность.

3) Специалисты в вашей области не имеют понятия об этой ответственности.

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

Если есть сомнения, оставьте это! Вы всегда можете выполнить рефакторинг позже, когда для этого есть четкое обоснование.

Как вы думаете?

Brandon
источник
Хотя эти рекомендации могут быть полезными, а могут и не быть полезными, на самом деле они не имеют ничего общего с SRP, как определено в SOLID, не так ли?
Сара
Благодарю вас. Я не могу поверить в какое-то безумие, связанное с так называемыми принципами SOLID, когда они делают очень простой код в сто раз более сложным, без веской причины . Точки, которые вы даете, описывают реальные причины внедрения SRP. Я думаю, что принципы, которые вы дали выше, должны стать их собственной аббревиатурой и выбросить «SOLID», это приносит больше вреда, чем пользы. «Архитектурные космонавты» действительно, как вы указали в теме, которая привела меня сюда.
Николас Петерсен
4

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

Джереми
источник
4

Ответ заключается в определении

То, что вы определяете ответственность, в конечном итоге дает вам границу.

Пример:

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

Если, с другой стороны, если я сказал ответственность за обработку счетов-фактур -> добавление нескольких небольших функций (например, печать счета- фактуры , обновление счета- фактуры ), все они находятся в пределах этой границы.

Однако если бы этот модуль начал обрабатывать какую-либо функциональность вне счетов , то он был бы за этой границей.

Темная ночь
источник
Я предполагаю, что главная проблема здесь - слово "обработка". Это слишком универсально, чтобы определить единственную ответственность. Я думаю, что было бы лучше иметь компонент, отвечающий за счет-фактуру на печать, а другой - счет-фактуру за обновление, а не отдельный дескриптор компонента {печать, обновление и - почему бы и нет? - показать, что угодно} счета .
Мачадо
1
ФП по существу спрашивает: «Как вы определяете ответственность?» поэтому, когда вы говорите, что ответственность - это то, что вы определяете, это кажется, что это просто повторяет вопрос.
Despertar
2

Я всегда смотрю на это на двух уровнях:

  • Я уверен, что мои методы делают только одну вещь и делают это хорошо
  • Я вижу класс как логическую (ОО) группу тех методов, которая хорошо представляет одну вещь

Итак, что-то вроде доменного объекта с именем Dog:

Dogэто мой класс, но собаки могут делать много вещей! У меня могут быть такие методы, как walk(), run()и bite(DotNetDeveloper spawnOfBill)(извините, не могу устоять; р).

Если это Dogстановится громоздким, то я бы подумал о том, как группы этих методов могут быть смоделированы вместе в другом классе, таком как Movementкласс, который может содержать мои walk()и run()методы.

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

Мартейн Вербург
источник
Bite действительно должен использовать экземпляр Object, а DotNetDeveloper должен быть подклассом Person (обычно так или иначе!)
Алан Пирс,
@ Алан - Там - исправил это для вас :-)
Мартейн Вербург
1

Я смотрю на это больше по линии класса, должен представлять только одну вещь. Присвоить @ Например Karianna, я есть свой Dogкласс, который имеет методы walk(), run()и bark(). Я не собираюсь добавлять методы meaow(), squeak(), slither()или fly()потому , что это не те вещи , которые делают собаки. Это вещи, которые делают другие животные, и у этих других животных были бы свои классы, чтобы представлять их.

(Кстати, если ваша собака действительно летает, то вам, вероятно, следует прекратить выбрасывать ее из окна).

JohnL
источник
+1 за "если ваша собака летит, то вам, вероятно, следует прекратить выбрасывать ее из окна". :)
Бобби Столы
Помимо вопроса о том, что должен представлять класс , что представляет экземпляр ? Если рассматривать SeesFoodкак характеристику DogEyes, Barkкак нечто, совершаемое a DogVoice, и Eatкак нечто, совершаемое a DogMouth, то if (dog.SeesFood) dog.Eat(); else dog.Bark();становится логически подобным if (eyes.SeesFood) mouth.Eat(); else voice.Bark();, утрачивая любое чувство идентичности, когда глаза, рот и голос связаны с одним правом.
суперкат
@supercat это справедливо, хотя контекст важен. Если код, который вы упоминаете, находится внутри Dogкласса, то он, вероятно, Dogсвязан. Если нет, то вы, вероятно, в конечном итоге что-то вроде, myDog.Eyes.SeesFoodа не просто eyes.SeesFood. Другая возможность - это Dogпредоставление ISeeинтерфейса, который требует Dog.Eyesсвойства и SeesFoodметода.
JohnL
@JohnL: Если фактическая механика видения обрабатывается глазами собаки, по сути, так же, как глазами кошки или зебры, то может иметь смысл управлять механикой Eyeклассом, но собака должна «видеть» используя свои глаза, а не просто глаза, которые могут видеть. Собака - это не глаз, но и не просто держатель глаз. Это «вещь, которую можно [хотя бы попытаться] увидеть», и она должна быть описана через интерфейс как таковая. Даже слепую собаку можно спросить, видит ли она еду; это не будет очень полезно, так как собака всегда скажет «нет», но спрашивать не вредно.
суперкат
Тогда вы будете использовать интерфейс ISee, как я описал в своем комментарии.
JohnL
1

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

Я использую имена классов в качестве теста для этого. Если я не могу дать классу достаточно короткое описательное имя или если в этом имени есть слово типа «И», я, вероятно, нарушаю принцип единой ответственности.

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

Дэвид Торнли
источник
0

Речь идет об одной уникальной роли .

Каждый класс должен быть возобновлен именем роли. На самом деле роль - это (набор) глаголов, связанных с контекстом.

Например :

Файл обеспечивает доступ к файлу. FileManager управляет объектами File.

Ресурс содержит данные для одного ресурса из файла. ResourceManager держать и предоставлять все ресурсы.

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

Итак, идея состоит лишь в том, чтобы дать вам простое представление о том, что делает класс, определив уникальную роль, которая может быть совокупностью нескольких под ролей (выполняемых объектами-членами или другими объектами).

Я часто строю классы Manager, в которых есть несколько других классов. Например, Фабрика, Реестр и т. Д. Посмотрите на класс Менеджера, как на своего рода руководителя группы, руководителя оркестра, который направляет другие народы работать вместе для достижения идеи высокого уровня. У него одна роль, но подразумевается работа с другими уникальными ролями внутри. Вы также можете увидеть, как организована компания: генеральный директор не является продуктивным на чистом уровне производительности, но если его там нет, то ничто не может работать правильно вместе. Это его роль.

Когда вы разрабатываете, определяйте уникальные роли. И для каждой роли, снова посмотрите, нельзя ли ее сократить в нескольких других ролях. Таким образом, если вам нужно просто изменить способ, которым ваш Менеджер строит объекты, просто измените Фабрику и действуйте спокойно.

Klaim
источник
-1

SRP - это не только разделение классов, но и делегирование функциональности.

В приведенном выше примере с собакой не используйте SRP в качестве оправдания, чтобы иметь 3 отдельных класса, таких как DogBarker, DogWalker и т. Д. (Низкая когезия). Вместо этого посмотрите на реализацию методов класса и решите, «знают ли они слишком много». У вас все еще может быть dog.walk (), но, вероятно, метод walk () должен делегировать другому классу детали того, как выполняется ходьба.

По сути, мы позволяем классу Dog иметь одну причину для изменения: потому что собаки меняются. Конечно, сочетая это с другими принципами SOLID, вы бы расширили Dog для новых функциональных возможностей, а не изменили Dog (открытый / закрытый). И вы бы внедрить ваши зависимости, такие как IMove и IEat. И, конечно, вы бы сделали эти отдельные интерфейсы (разделение интерфейса). Dog будет меняться только в том случае, если мы обнаружим ошибку или если Dogs кардинально изменится (Liskov Sub, не расширяйте, а затем удаляйте поведение).

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

user1488779
источник
-1

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

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

Итак, если мы определяем ответственность более конкретно, нам не нужно ее менять.

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

Предположим, мы добавили еще одну ответственность в класс Dog с именем catchThief (). Теперь это может привести к дополнительной другой ответственности. Завтра, если полицейский отдел должен поменять способ, которым собака ловит вора, то класс собаки должен быть изменен. В этом случае было бы лучше создать другой подкласс и назвать его ThiefCathcerDog. Но, с другой стороны, если мы уверены, что он не изменится ни при каких обстоятельствах, или способ реализации catchThief зависит от какого-то внешнего параметра, тогда вполне нормально взять на себя эту ответственность. Если обязанности не являются поразительно странными, то мы должны решить их разумно на основе варианта использования.

АКС
источник
-1

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

kiwicomb123
источник
1
Одна из причин изменения не имеет ничего общего с количеством вариантов использования, действующих лиц или вероятностью изменения. Вы не должны составлять список вероятных изменений. Это верно, что одно изменение должно влиять только на один класс. Возможность расширить класс, чтобы учесть это изменение, это хорошо, но это принцип открытого закрытого типа, а не SRP.
candied_orange
Почему бы нам не составить список наиболее вероятных изменений? Спекулятивный дизайн?
kiwicomb123
потому что это не поможет, не будет полным, и есть более эффективные способы справиться с изменением, чем пытаться предсказать его. Просто ожидай это. Изоляция решений и влияние будет минимальным.
candied_orange
Ладно, я понимаю, это нарушает принцип "тебе не нужно это".
kiwicomb123
На самом деле, ягни может быть продвинута далеко. Это действительно предназначено для того, чтобы удержать вас от реализации спекулятивных вариантов использования Не мешать вам изолировать реализованный вариант использования.
candied_orange