При использовании принципа единой ответственности, что представляет собой «ответственность»?


199

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

public Interface CustomerCRUD
{
    public void Create(Customer customer);
    public Customer Read(int CustomerID);
    public void Update(Customer customer);
    public void Delete(int CustomerID);
}

Боб Мартин говорит, что «у классов должна быть только одна причина измениться». Но это сложно обдумать, если вы новичок в SOLID.

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

Так как вы это делаете? Как вы определяете, какие обязанности должен иметь каждый класс, и как вы определяете ответственность в контексте ПСП?


28
Опубликовать в Code Review и быть разорванным на части :-D
Йорг W Mittag

8
@ JörgWMittag Эй, не пугай людей :)
Flambino

118
Подобные вопросы от ветеранов-членов демонстрируют, что правила и принципы, которых мы пытаемся придерживаться, ни в коем случае не являются прямыми или простыми . Они [своего рода] противоречивы и мистичны ... как и должно быть в любом хорошем наборе правил . И я хотел бы верить таким смиренным вопросам и дать надежду тем, кто чувствует себя безнадежно глупым. Спасибо, Роберт!
svidgen

41
Интересно, если бы этот вопрос был бы опущен + сразу помечен как дубликат, если бы он был опубликован нубом :)
Andrejs

9
@rmunn: или, другими словами, большой представитель привлекает еще больше представителей, потому что никто не отменял основные человеческие предубеждения при обмене стеками
Andrejs

Ответы:


117

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

Например:

Новое деловое требование: пользователи, находящиеся в Калифорнии, получают специальную скидку.

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

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

Или же:

Новое нефункциональное требование: мы начнем использовать Oracle вместо SQL Server

Пример хорошего изменения: просто нужно изменить один класс на уровне доступа к данным, который определяет, как сохранить данные в DTO.

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

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

Как минимум, ваши занятия должны отделять логические проблемы от физических. Большой набор примеров можно найти в System.IOпространстве имен: там можно найти различные виды физических потоков (например FileStream, MemoryStreamили NetworkStream) и различных читателей и писателей ( BinaryWriter, TextWriter) , которые работают на логическом уровне. Разделяя их таким образом, мы избегаем комбинаторного взрыва: вместо того , FileStreamTextWriter, FileStreamBinaryWriter, NetworkStreamTextWriter, NetworkStreamBinaryWriter, MemoryStreamTextWriter, и MemoryStreamBinaryWriter, вы просто подключить писатель и поток , и вы можете иметь то , что вы хотите. Затем мы можем добавить, скажем, XmlWriterбез необходимости повторной реализации для памяти, файлов и сети отдельно.


34
Хотя я согласен с перспективой на будущее, существуют такие принципы, как YAGNI, а методологии, такие как TDD, предполагают обратное.
Роберт Харви,

87
YAGNI говорит нам не создавать вещи, которые нам не нужны сегодня. Это не говорит о том, чтобы не создавать вещи способом, который является расширяемым. См. Также принцип «открыт / закрыт» , в котором говорится, что «программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации».
Джон Ву

18
@JohnW: +1 только за ваш комментарий к ЯГНИ. Я не могу поверить, насколько я должен объяснять людям, что YAGNI не является оправданием для создания жесткой, негибкой системы, которая не может реагировать на изменения - по иронии судьбы, прямо противоположная тому, к чему стремятся SRP и открытые / закрытые принципы.
Грег Бургхардт

36
@JohnWu: Я не согласен, YAGNI говорит нам точно не создавать вещи, которые нам не нужны сегодня. Например, читаемость и тесты - это то, что программе всегда нужно «сегодня», поэтому YAGNI никогда не оправдывает не добавлять структуру и точки внедрения. Тем не менее, как только «расширяемость» добавляет значительную стоимость, для которой преимущества не очевидны «сегодня», YAGNI означает, что следует избегать такого рода расширяемости, так как последнее ведет к чрезмерному повышению квалификации.
Док Браун

9
@JohnWu Мы перешли с SQL 2008 на 2012. Всего было два запроса, которые нужно было изменить. А из SQL Auth доверять? Почему это может быть даже изменение кода; достаточно изменить connectionString в конфигурационном файле. Опять ЯГНИ. YAGNI и SRP иногда конкурируют друг с другом, и вам нужно решить, какая из них имеет лучшую цену / выгоду.
Энди

76

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

Речь идет о том, что, по вашему опыту , может измениться.

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

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

Когда дядя Боб пишет об этом, он предлагает нам подумать об «ответственности» с точки зрения «кто просит перемен». Другими словами, мы не хотим, чтобы Партия А потеряла свою работу, потому что Партия Б потребовала перемен.

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

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

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


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


11
Лучший ответ, и на самом деле цитирует мысли дяди Боба. Что касается того, что может измениться, то все делают большие проблемы с вводом / выводом, «что если мы изменим базу данных?» или "что если мы перейдем с XML на JSON?" Я думаю, что это обычно ошибочно. Реальный вопрос должен звучать так: «Что если нам нужно изменить это int на число с плавающей точкой, добавить поле и изменить эту строку на список строк?»
user949300

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

6
@Basilevs Я пытаюсь разобраться в недостатке, который вы видите в этом ответе - не говоря уже об ответе дяди Боба! Но, возможно, мне нужно уточнить, что SRP не означает, что «изменение» повлияет только на 1 класс. Речь идет о том, чтобы каждый класс отвечал только на «одно изменение». ... Речь идет о попытке нарисовать ваши стрелки от каждого класса к одному владельцу. Не от каждого владельца до одного класса.
svidgen

2
Спасибо за прагматичный ответ! Даже дядя Боб предостерегает от ревностного соблюдения принципов SOLID в Agile Architecture . У меня нет удобной цитаты, но он в основном говорит, что разделение обязанностей по своей сути повышает уровень абстракции в вашем коде и что вся абстракция обходится дорого, поэтому убедитесь, что польза от следования SRP (или другим принципам) перевешивает затраты добавить больше абстракции. (продолжение следующего комментария)
Майкл Л.

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

29

Я следую "у классов должна быть только одна причина измениться".

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

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

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

Другая грубая метрика - высота лифта. Традиционные элеваторные передачи - «если вы были в лифте с инвестором, можете ли вы продать его по идее?». У стартапов должно быть простое, краткое описание того, что они делают - какова их цель. Аналогично, классы (и функции) должны иметь простое описание того, что они делают . Не «этот класс реализует некоторый fubar такой, что вы можете использовать его в этих конкретных сценариях». Что-то, что вы можете сказать другому разработчику: «Этот класс создает пользователей». Если вы не можете сообщить , что другим разработчикам, вы собираетесь получить ошибки.


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

16
Я большой сторонник идеи "элеватора". Если вам сложно объяснить, что делает класс в предложении или двух, вы находитесь на рискованной территории.
Иван

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

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

1
@KevinKrumwiede Вот для чего нужны методологии «Цыпленок бегает с отрубленной головой» и «Погоня за дикими гусями»!

26

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

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

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


20
Новые программисты имеют тенденцию рассматривать SOLID, как если бы это был набор законов, а это не так. Это просто группа хороших идей, чтобы помочь людям стать лучше в классе. Увы, люди склонны воспринимать эти принципы слишком серьезно; Недавно я увидел объявление о вакансии, в котором указано, что SOLID является одним из требований к работе.
Роберт Харви

9
+42 за последний абзац. Как говорит @RobertHarvey, такие вещи, как SPR, SOLID и YAGNI, следует воспринимать не как « абсолютные правила », а как общие принципы «хорошего совета». Между ними (и другими) совет иногда будет противоречивым, но балансировка этого совета (в отличие от следования жесткому набору правил) со временем (по мере роста вашего опыта) будет направлять вас на создание более качественного программного обеспечения. В разработке программного обеспечения должно быть только одно «абсолютное правило»: « Абсолютных правил не существует ».
TripeHound

Это очень хорошее разъяснение по одному аспекту ПСП. Но, даже если принципы SOLID не являются жесткими правилами, они не очень ценны, если никто не понимает, что они имеют в виду - тем более, если ваше утверждение, что «никто не знает», действительно верно! ... для них имеет смысл быть трудным для понимания. Как и в любом навыке, есть нечто, что отличает хорошее от менее хорошего! Но ... «никто не знает» делает это скорее неуставным ритуалом. (И я не верю, что это цель SOLID!)
svidgen

3
«Никто не знает», я надеюсь, что @Euphoric просто означает, что нет точного определения, которое будет работать для каждого варианта использования. Это то, что требует определенной степени суждения. Я думаю, что один из лучших способов определить, в чем заключаются ваши обязанности, - это быстро выполнять итерации, и пусть ваша кодовая база скажет вам . Ищите «запахи», которые ваш код не легко поддерживать. Например, когда изменение одного бизнес-правила начинает оказывать каскадное воздействие через, казалось бы, не связанные классы, у вас, вероятно, есть нарушение SRP.
Майкл Л.

1
Я сердечно второй @TripeHound и другие, которые указали, что все эти «правила» существуют не для определения Единой Истинной Религии развития, а для повышения вероятности разработки поддерживаемого программного обеспечения. Будьте очень осторожны в следовании «лучшей практике», если вы не можете объяснить, как она продвигает поддерживаемое программное обеспечение, улучшает качество или повышает эффективность разработки.
Майкл Л.

5

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

  • Ответственность соразмерна власти.
  • Никакие две организации не должны нести ответственность за одно и то же.

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

В дополнение к этим двум, третий принцип кажется разумным:

  • Ответственность может быть делегирована

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

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


Я не уверен на 100%, что это полностью объясняет это. Но я думаю, что объяснение «ответственности» в отношении «власти» - проницательный способ выразить это! (+1)
свидген

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

@nocomprende Вы также склонны встраивать свои сильные стороны в машину. Я бы сказал, что когда ваши сильные и слабые стороны - это одно и то же, тогда становится интересно.
Cort Ammon

5

На этой конференции в Йельском университете дядя Боб приводит такой забавный пример:

Введите описание изображения здесь

Он говорит, что Employeeесть три причины для изменения, три источника требований к изменениям, и дает это юмористическое и насмешливое , но, тем не менее, иллюстративное объяснение:

  • Если CalcPay()метод содержит ошибку и стоит компании миллионы долларов США, финансовый директор уволит вас .

  • Если ReportHours()метод содержит ошибку и стоит компании миллионы долларов США, COO уволит вас .

  • Если WriteEmmployee(метод) имеет ошибку, которая приводит к удалению большого количества данных и стоит компании миллионы долларов США, технический директор уволит вас .

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

Он дает это решение, которое устраняет нарушение SRP, но все же должно устранить нарушение DIP, которое не показано на видео.

Введите описание изображения здесь


Этот пример больше похож на класс с неправильными обязанностями.
Роберт Харви

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

5
Я слышу, что вы говорите, но я не нахожу это убедительным. Есть разница между классом, имеющим слишком много обязанностей, и классом, занимающимся чем-то, чего он вообще не имеет. Это может звучать одинаково, но это не так; считать арахис не то же самое, что называть их грецкими орехами. Это принцип дяди Боба и пример дяди Боба, но если бы он был достаточно описательным, нам бы этот вопрос вообще не понадобился.
Роберт Харви,

@RobertHarvey, в чем разница? Эти ситуации кажутся мне изоморфными.
Пол Дрэйпер

3

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

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

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

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


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

1
Автомобильная аналогия: мне не нужно знать, сколько бензина находится в чужом баке, или хочу включить чужие дворники. (но это определение интернета) (Шшш! ты

1
@nocomprende - «Мне не нужно знать, сколько бензина в чьем-то баке», - если только вы не подросток, который пытается решить, какую из семейных машин «одолжить» для вашей следующей поездки ...;)
alephzero

3

SRP трудно понять правильно. Это в основном вопрос назначения «заданий» вашему коду и обеспечения четкой ответственности каждой части. Как и в реальной жизни, в некоторых случаях разделение работы между людьми может быть вполне естественным, но в других случаях это может быть очень сложно, особенно если вы не знаете их (или работу).

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

Одна вещь, которую вы заметите, это когда модуль начинает делать слишком много и его трудно отлаживать / поддерживать. Это момент для рефакторинга; какой должна быть основная работа и какие задачи можно поручить другому модулю? Например, должен ли он обрабатывать проверки безопасности и другую работу, или вы должны сначала выполнить проверки безопасности в другом месте, или это сделает код более сложным?

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


Разве SRP не только большой приказ Сплоченности Константина?
Ник Кейли,

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

@NickKeighley Я думаю, что это сплоченность, не такая большая, но с другой точки зрения.
Сденхэм

3

«Принцип единой ответственности», возможно, вводит в заблуждение. «Только одна причина для изменения» - лучшее описание принципа, но его легко понять неправильно. Мы не говорим о том, что заставляет объекты изменять состояние во время выполнения. Мы думаем о том, что может заставить разработчиков в будущем менять код.

Если мы не исправим ошибку, изменение будет связано с новым или измененным бизнес-требованием. Вам придется думать вне самого кода и представить, какие внешние факторы могут привести к тому, что требования изменятся независимо . Сказать:

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

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

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

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

Цитирую дядю Боба, который придумал этот термин:

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

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


Согласно его книге «Чистая архитектура», это совершенно верно. Бизнес-правила должны исходить из одного источника и только из одного источника. Это означает, что персонал, отдел операций и ИТ должны сотрудничать при формулировании требований в рамках «единой ответственности». И это принцип. +1
Бенни Скогберг

2

Хорошая статья, которая объясняет принципы программирования SOLID и дает примеры кода, которые следуют и не следуют этим принципам, - https://scotch.io/bar-talk/solid-the-first-five-principles-of-object-oriented- дизайн .

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

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

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

Это простой пример (и его легче понять при чтении статьи, поскольку он содержит фрагменты кода), но он демонстрирует основную идею SRP.


0

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

Интерфейсы

У вас есть этот интерфейс:

public Interface CustomerCRUD
{
  public void Create(Customer customer);
  public Customer Read(int CustomerID);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Предположительно, у вас есть несколько классов, которые соответствуют CustomerCRUDинтерфейсу (в противном случае интерфейс не требуется), и некоторая функция, do_crud(customer: CustomerCRUD)которая принимает соответствующий объект. Но вы уже нарушили SRP: вы связали эти четыре различные операции вместе.

Допустим, позже вы будете работать с представлениями базы данных. Представление базы данных имеет только на Readдоступный метод для этого. Но вы хотите написать функцию, do_query_stuff(customer: ???)которая прозрачно оперирует с полнофункциональными таблицами или представлениями; в Readконце концов, он использует только метод.

Так что создайте интерфейс

public Interface CustomerReader {общедоступное чтение клиента (customerID: int)}

и фактор вашего CustomerCrudинтерфейса как:

public interface CustomerCRUD extends CustomerReader
{
  public void Create(Customer customer);
  public void Update(Customer customer);
  public void Delete(int CustomerID);
}

Но конца не видно. Могут быть объекты, которые мы можем создавать, но не обновлять и т. Д. Эта кроличья нора слишком глубокая. Единственный разумный способ придерживаться принципа единой ответственности - сделать так, чтобы все ваши интерфейсы содержали ровно один метод . Go фактически следует этой методологии из того, что я видел, с подавляющим большинством интерфейсов, содержащих одну функцию; Если вы хотите указать интерфейс, который содержит две функции, вы должны неловко создать новый интерфейс, который объединяет эти две функции. Вы скоро получите комбинаторный взрыв интерфейсов.

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

let do_customer_stuff customer = customer.read ... customer.update ...

это вызывает любые методы, которые нам нравятся. OCaml будет использовать вывод типа, чтобы определить, что мы можем передать любой объект, который реализует эти методы. В этом примере было бы определить, что customerимеет тип <read: int -> unit, update: int -> unit, ...>.

Классы

Это решает интерфейсный беспорядок; но мы все еще должны реализовать классы, которые содержат несколько методов. Например, должны ли мы создать два разных класса, CustomerReaderа CustomerWriter? Что если мы захотим изменить способ чтения таблиц (например, теперь мы кэшируем наши ответы в redis до получения данных), но теперь как они пишутся? Если вы будете следовать этой цепочке рассуждений до логического завершения, вы будете неразрывно связаны с функциональным программированием :)


4
«Бессмысленно» немного сильно. Я мог бы отстать от «мистического» или «дзен». Но не совсем бессмысленно!
svidgen

Можете ли вы объяснить немного больше, почему структурный подтип является решением?
Роберт Харви,

@RobertHarvey Реструктурировал мой ответ значительно
садовник

4
Я использую интерфейсы, даже когда у меня есть только один класс, реализующий его. Почему? Насмешка в юнит-тестах.
Eternal21

0

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

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


0

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

Но удачи, понимая, что на первый взгляд, когда вы впервые услышите о SOLID.


Я вижу много комментариев, в которых говорится, что SRP и YAGNI могут противоречить друг другу, но YAGN, который я соблюдаю при помощи TDD (GOOS, London School), научил меня думать и разрабатывать свои компоненты с точки зрения клиента. Я начал проектировать свои интерфейсы по наименьшему из пожеланий любого клиента, вот как мало он должен делать . И это упражнение может быть сделано без каких-либо знаний TDD.

Мне нравится техника, описанная дядей Бобом (к сожалению, я не могу вспомнить откуда), которая выглядит примерно так:

Спросите себя, что делает этот класс?

Ваш ответ содержал либо И, либо Или

Если это так, извлеките эту часть ответа, это является собственной ответственностью

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


Я думаю, что многие ответы приводят аргумент в пользу разъединения, когда речь идет о SRP .

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

Теоретически, без SRP у вас не будет никаких зависимостей ...

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

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


3
When learning something new, absolutes are the best, it is easier to just always do something.- По моему опыту, новые программисты слишком догматичны. Абсолютизм ведет к не думающим разработчикам и к культу культового программирования. Сказать «просто сделай это» - это хорошо, если ты понимаешь, что человеку, с которым ты разговариваешь, придется позже отучиться от того, чему ты их научил.
Роберт Харви,

@RobertHarvey, Совершенно верно, это создает догматическое поведение, и вы должны отучиться / переучиваться, когда вы получаете опыт. Это моя точка зрения, хотя. Если новый программист пытается делать суждения без какого-либо способа обосновать свое решение, это кажется бесполезным, потому что они не знают, почему это сработало, когда сработало. Заставляя людей переусердствовать , он учит их искать исключения, а не делать неквалифицированные предположения. Все, что вы сказали об абсолютизме, правильно, поэтому оно должно быть только отправной точкой.
Крис Волерт,

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

0

На это нет однозначного ответа. Хотя вопрос узкий, объяснения нет.

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

Что это значит практически?

В последнее время я использую стиль кодирования, который состоит в основном из двух фаз:

Фаза I лучше всего описана как творческий хаос. На этом этапе я записываю код, потому что мысли текут - то есть сыры и безобразны.

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

Сейчас я работаю в основном на Python, что позволяет мне думать об объектах и ​​классах позже. Первая фаза I - я пишу только функции и выкладываю их почти случайно в разных модулях. На втором этапе , после того, как я приступил к работе, я более подробно рассмотрю, какой модуль имеет дело с какой частью решения. И пока я просматриваю модули, темы меняются. Некоторые функции тематически связаны. Это хорошие кандидаты на занятия . И после того, как я превратил функции в классы - что почти сделано с отступом и добавлением selfв список параметров в python;) - я использую SRPкак Razor Оккама, чтобы выделить функциональность для других модулей и классов.

Текущий пример может писать небольшую функциональность экспорта на днях.

Была необходимость в CSV , Excel и комбинированных листах Excel в формате ZIP.

Простая функциональность была выполнена в трех видах (= функции). Каждая функция использовала общий метод определения фильтров и второй метод для извлечения данных. Затем в каждой функции выполнялась подготовка экспорта, которая доставлялась в виде ответа от сервера.

Было перепутано слишком много уровней абстракции:

I) работа с входящим / исходящим запросом / ответом

II) определение фильтров

III) получение данных

IV) преобразование данных

Легким шагом было использование одной абстракции ( exporter) для работы со слоями II-IV на первом этапе.

Осталась только тема, касающаяся запросов / ответов . На том же уровне абстракции извлекает параметры запроса, что нормально. Так что я имел за это мнение одну «ответственность».

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

Определение критериев фильтра и фактическое извлечение почти на одном уровне абстракции (фильтры необходимы для получения правильного подмножества данных). Эти уровни были помещены в нечто вроде уровня доступа к данным .

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

По мере формирования классов и модулей все стало яснее, что и где принадлежало. И всегда скрытый вопрос, делает ли класс слишком много .

Как вы определяете, какие обязанности должен иметь каждый класс, и как вы определяете ответственность в контексте ПСП?

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

В основном для меня это своего рода «художественная интуиция», которая ведет к текущему дизайну; Я моделирую код, как художник, который лепит глину или рисует.

Представь меня кодирующим Бобом Россом ;)


0

Что я пытаюсь сделать, чтобы написать код, который следует SRP:

  • Выберите конкретную проблему, которую нужно решить;
  • Напишите код, который его решает, напишите все одним методом (например, main);
  • Тщательно проанализируйте код и, основываясь на бизнесе, попытайтесь определить обязанности, которые видны во всех выполняемых операциях (это субъективная часть, которая также зависит от бизнеса / проекта / клиента);
  • Обратите внимание, что весь функционал уже реализован; что дальше - это только организация кода (с этого момента в этом подходе не будет реализовано никаких дополнительных функций или механизмов);
  • На основе обязанностей, которые вы определили на предыдущих шагах (которые определены на основе бизнеса и идеи «одна причина изменить»), извлеките отдельный класс или метод для каждого;
  • Обратите внимание, что этот подход заботит только SPR; в идеале здесь должны быть дополнительные шаги, чтобы попытаться придерживаться и других принципов.

Пример:

Проблема: получить от пользователя два числа, вычислить их сумму и вывести результат пользователю:

//first step: solve the problem right away
static void Main(string[] args)
{
    Console.WriteLine("Number 1: ");
    int firstNumber = Convert.ToInt32(Console.ReadLine());

    Console.WriteLine("Number 2: ");
    int secondNumber = Convert.ToInt32(Console.ReadLine());

    int result = firstNumber + secondNumber;

    Console.WriteLine("Hi there! The result is: {0}", result);

    Console.ReadLine();
}

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

//Responsible for getting two integers from the user
class Input {
    public int FirstNumber { get; set; }
    public int SecondNumber { get; set; }
    public void Read() {
        Console.WriteLine("Number 1: ");
        FirstNumber = Convert.ToInt32(Console.ReadLine());

        Console.WriteLine("Number 2: ");
        SecondNumber = Convert.ToInt32(Console.ReadLine());
    }
}

//Responsible for calculating the sum of two integers
class SumOperation {
    public int Result { get; set; }
    public void Calculate(int a, int b) {
        Result = a + b;
    }
}

//Responsible for the output of some value to the user
class Output {
    public void Write(int result) {
        Console.WriteLine("Hello! The result is: {0}", result);
    }
}

Затем переработанная программа становится:

//Program: responsible for main execution.
//Gets two numbers from user and output their sum.
static void Main(string[] args)
{
    var input = new Input();
    input.Read();

    var operation = new SumOperation();
    operation.Calculate(input.FirstNumber, input.SecondNumber);

    var output = new Output();
    output.Write(operation.Result);

    Console.ReadLine();
}

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


1
Ваш пример слишком прост, чтобы адекватно проиллюстрировать SRP. Никто не будет делать это в реальной жизни.
Роберт Харви

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

0

Из книги Роберта К. Мартинса « Чистая архитектура: руководство мастера по структуре и дизайну программного обеспечения» , опубликованной 10 сентября 2017 года, Роберт пишет на странице 62 следующее:

Исторически SRP был описан так:

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

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

Модуль должен отвечать перед одним и только одним пользователем или заинтересованным лицом.

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

Таким образом, окончательная версия SRP:

Модуль должен отвечать за одного и только одного актера.

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


Я не уверен, почему вы делаете различие, что «дело не в коде». Конечно, это о коде; это разработка программного обеспечения.
Роберт Харви

@RobertHarvey Я хочу сказать, что поток требований исходит из одного источника - актера. Пользователи и заинтересованные стороны - не в коде, а в бизнес-правилах, которые предъявляются нам в качестве требований. Таким образом, SRP - это процесс контроля этих требований, который для меня не является кодом. Это разработка программного обеспечения (!), Но не код.
Бенни Скогберг
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.