Может / должен ли Принцип единой ответственности применяться к новому коду?

20

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

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

SeeNoWeevil
источник
6
@Frank - это на самом деле обычно определяется так - см., Например, en.wikipedia.org/wiki/Single_responsibility_principle
Joris Timmermans
1
То, как вы это выражаете, не совсем так, как я понимаю определение SRP.
Питер Б
2
Каждая строка кода имеет (как минимум) две причины для изменения: это приводит к ошибке или мешает новому требованию.
Барт ван Инген Шенау
1
@BartvanIngenSchenau: LOL ;-) Если вы видите это таким образом, SRP нигде не может быть применен.
Док Браун
1
@DocBrown: Вы можете, если не связываете SRP с изменением исходного кода. Например, если вы интерпретируете SRP как способную дать полное представление о том, что делает класс / функция в одном предложении, без использования слова и (и без формулировки ласки, чтобы обойти это ограничение).
Барт ван Инген Шенау

Ответы:

27

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

По моему опыту, применение SRP дает вам преимущество намного раньше: когда вам нужно выяснить, где и как применить конкретное изменение в вашем коде. Для этого вы должны прочитать и понять ваши существующие функции и классы. Это становится намного проще, когда все ваши функции и классы несут особую ответственность. ИМХО, вам следует применять SRP всякий раз, когда он облегчает чтение вашего кода, всякий раз, когда он делает ваши функции меньше и более самоописываемыми. Таким образом, ответ - да , имеет смысл применять SRP даже для нового кода.

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

 void RunPrintWorkflow()
 {
     var document = ReadDocument();
     var formattedDocument = FormatDocument(document);
     PrintDocumentToScreen(formattedDocument);
 }

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

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

РЕДАКТИРОВАТЬ: к вашему комментарию: Основная обязанность внешней функции в приведенном выше примере - не печатать на определенном устройстве или не форматировать документ, а интегрировать рабочий процесс печати . Таким образом, на уровне абстракции внешней функции новое требование типа «документы не должны больше форматироваться» или «документ должен быть отправлен по почте вместо печати» - это просто «та же причина», а именно «рабочий процесс печати изменился». Если мы говорим о таких вещах, важно придерживаться правильного уровня абстракции .

Док Браун
источник
Я обычно всегда развиваюсь с TDD, поэтому в моем примере я не смог бы физически хранить всю эту логику в одном модуле, потому что было бы невозможно протестировать. Это всего лишь побочный продукт TDD, а не потому, что я намеренно применяю SRP. Мой пример имел довольно четкие, отдельные обязанности, так что, возможно, это не очень хороший пример. Я думаю, что я спрашиваю, можете ли вы написать какой-нибудь новый фрагмент кода и однозначно сказать, да, это не нарушает SRP? Разве «причины перемен» по сути не определены бизнесом?
SeeNoWeevil
3
@thecapsaicinkid: да, вы можете (по крайней мере, путем немедленного рефакторинга). Но вы получите очень, очень маленькие функции - и не всем программистам это нравится. Посмотрите этот пример: sites.google.com/site/unclebobconsultingllc/…
Док Браун
Если бы вы применяли SRP, предвидя причины для изменений, в вашем примере я все еще мог бы утверждать, что у него более одной причины. Бизнес может решить, что больше не хочет форматировать документ, а затем решить, что он хочет, чтобы он был отправлен по электронной почте, а не напечатан. РЕДАКТИРОВАТЬ: Просто прочитайте ссылку, и, хотя мне не особенно нравится конечный результат, «Извлекать, пока вы просто не можете больше извлекать», имеет гораздо больше смысла и менее двусмысленно, чем «только одна причина для изменения». Не очень прагматично, хотя.
SeeNoWeevil
1
@thecapsaicinkid: см. мое редактирование. Основная обязанность внешней функции - не печатать на определенном устройстве или не форматировать документ, а интегрировать рабочий процесс печати. И когда этот рабочий процесс меняется, это единственная причина, по которой функция будет меняться
Док Браун,
Ваш комментарий о том, что вы придерживаетесь правильного уровня абстракции, кажется, я пропустил. Например, у меня есть класс, который я бы описал как «Создает структуры данных из массива JSON». Звучит как единственная ответственность передо мной. Перебирает объекты в массиве JSON и отображает их в POJO. Если я придерживаюсь того же уровня абстракции, что и мое описание, трудно утверждать, что у него есть более чем одна причина для изменения, а именно: «Как JSON сопоставляется с объектом». Будучи менее абстрактным, я могу утверждать, что у этого есть более чем одна причина, например, как я отображаю изменения полей даты, как числовые значения сопоставляются с днями и т. Д.
SeeNoWeevil
7

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

Единственная причина изменений не в изменении кода, а в том, что делает ваш код.

Питер Б
источник
3

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

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

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

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

Адриан
источник
3

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

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

Рефакторинг для уменьшения кода

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

Создать яблочко

Шаблон одноразового использования был предназначен для сокращения исходного кода и улучшения повторного использования кода. Он должен был создать специализацию и конкретные реализации. Этакий bullseyeисходный код для вас go to specific tasks. Когда возникла проблема с печатью, вы точно знали, куда идти, чтобы ее исправить.

Разовое использование не означает неоднозначного разрушения

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

Вы уверены, что usageзначительно изменился?

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

Сможет ли новый парень быстро это выяснить?

Всегда поддерживайте ваш исходный код в самой простой для понимания организации.

Не будь часовщиком

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

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

Reactgular
источник
2

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

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

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

Конечно, ЯГНИ говорит тебе, что не стоит. Вам нужно найти баланс между принципами дизайна.

Ян Худек
источник
2

Флюп движется в правильном направлении. «Принцип единой ответственности» изначально применялся к процедурам. Например, Деннис Ритчи сказал бы, что функция должна делать одну вещь и делать это хорошо. Затем в C ++ Бьярн Страуструп сказал бы, что класс должен делать одну вещь и делать это хорошо.

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

Современные реализации (т. Е. Agile и DDD) больше ориентированы на то, что важно для бизнеса, чем на то, что может выразить язык программирования. Удивительным моментом является то, что языки программирования еще не догнали. Старые языки, подобные Фортрану, берут на себя обязанности, которые соответствуют основным концептуальным моделям того времени: процессы, применяемые к каждой карточке, когда она проходила через устройство чтения карточек, или (как в С) обработку, сопровождающую каждое прерывание. Затем появились языки ADT, которые созрели для того, чтобы уловить то, что люди DDD позже заново изобрели как важные (хотя Джим Соседы многое из этого выяснили, опубликовали и использовали к 1968 году): то, что сегодня мы называем классами , (Они НЕ модули.)

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

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

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

Вот почему я в восторге от архитектуры DCI Trygve Reenskaug, которая описана в книге Lean Architecture выше. Наконец, это дает некоторую реальную оценку тому, что раньше было произвольным и мистическим несоблюдением «единой ответственности» - как можно найти в большинстве рассуждений выше. Этот статус относится к человеческим ментальным моделям: сначала конечные пользователи И вторые программисты. Это относится к бизнесу. И, почти случайно, он заключает в себе изменения, так как флоп бросает нам вызов.

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

Справиться
источник
2
Читая то, что вы написали, где-то по пути я просто полностью потерял из виду то, о чем вы говорите. Хорошие ответы не рассматривают вопрос как отправную точку для бродяги в лесу, а скорее как определенную тему, чтобы связать всю письменность.
Donal Fellows
1
Ах, ты один из них, как один из моих старых менеджеров. «Мы не хотим это понимать: мы хотим улучшить это!» Ключевой тематический вопрос здесь один из принципиальных: это «P» в «SRP». Возможно, я бы прямо ответил на вопрос, если бы это был правильный вопрос: это не так. Вы можете обсудить это с тем, кто когда-либо задавал вопрос.
справиться
Там где-то есть хороший ответ. Я думаю ...
RubberDuck
0

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

Но! Что такое ответственность?

Является ли «печать отчета ответственность»? Ответ, я полагаю, «Возможно».

Давайте попробуем использовать определение SRP как «имеющего только одну причину для изменения».

Предположим, у вас есть функция, которая печатает отчеты. Если у вас есть два изменения:

  1. измените эту функцию, потому что ваш отчет должен иметь черный фон
  2. измените эту функцию, потому что вам нужно распечатать в PDF

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

Но если бы ваше второе изменение было бы:

2b. измените эту функцию, потому что вашему отчету нужен другой шрифт

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

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

Sarien
источник
0

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

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

Отличное чтение об этих вещах: Lean Architecture от Coplien

flup
источник
0

«Печать» очень похожа на «просмотр» в MVC. Любой, кто понимает основы предметов, поймет это.

Это системная ответственность. Он реализован в виде механизма - MVC - который включает в себя принтер (представление), печатаемую вещь (модуль) и запрос и параметры принтера (из контроллера).

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

Справиться
источник
0

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

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

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

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

Это строго указывает на то, что общая ответственность - «печать» кода - имеет подответственность и должна быть разбита на части. Это не нарушение SRP само по себе, а скорее указание на то, что разделение (возможно, на «форматирование» и «рендеринг» подзадач), вероятно, требуется. Можете ли вы четко описать эти обязанности, чтобы понять, что происходит внутри подзадач, не глядя на их выполнение? Если вы можете, они, вероятно, будут разумным расколом.

Также может быть понятнее, если мы посмотрим на простой реальный пример. Давайте рассмотрим sort()метод полезности в java.util.Arrays. Что оно делает? Он сортирует массив, и это все, что он делает. Он не печатает элементы, он не находит наиболее морально подходящего члена, он не свистит Дикси . Это просто сортирует массив. Вы не должны знать, как либо. Сортировка - единственная ответственность этого метода. (На самом деле, в Java есть много методов сортировки по несколько уродливым техническим причинам, связанным с примитивными типами; вам не нужно обращать на это никакого внимания, поскольку все они имеют эквивалентные обязанности.)

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

Donal Fellows
источник