Непростой принцип единственной ответственности

Автор оригинала: Владимир Репин
  • Перевод

Предыстория


За последние пару лет я поучаствовал в немалом количестве собеседований. На каждом из них я спрашивал соискателей о принципе единственной ответственности(далее SRP). И большинство людей о принципе ничего не знают. И даже из тех, кто мог зачитать определение, почти никто не мог сказать как они используют этот принцип в своей работе. Не могли сказать, как SRP влияет на код, который они пишут или на ревью кода коллег. Некоторые из них также имели заблуждение, что SRP, как и весь SOLID, имеет отношение только к объектно ориентированному программированию. Также, зачастую люди не могли определить явные случаи нарушения этого принципа, просто потому что код был написан в стиле, рекомендованном известным фреймворком.
Redux — яркий пример фреймворка, гайдлайн которого нарушает SRP.

SRP имеет значение


Хочу начать с ценности этого принципа, с пользы которую он несет. А также хочу отметить, что принцип распространяется не только на ООП, но и на процедурное программирование, функциональное и даже декларативное. HTML, как представителя последнего тоже можно и нужно декомпозировать, тем более сейчас, когда он управляется UI-фреймворками, такими как React или Angular. Кроме этого принцип распространяется и на другие инженерные области. И не только инженерные, было такое выражение в военной тематике: «divide and conquer», что по большому счету воплощение того же принципа. Сложность убивает, раздели ее на части и ты победишь.
Касательно же других инженерных областей, здесь, на хабре, была интересная статья о том как у разрабатываемого самолета отказали двигатели, не перешли на реверс по команде пилота. Проблема была в том, что они неверно интерпретировали состояние шасси. Вместо того, чтобы полагаться на системы контролирующие шасси, контроллер двигателя напрямую считывал датчики, концевики и пр. находящиеся в шасси. Также в статье было упомянуто, что двигатель должен проходить длительную сертификацию до того как его поставят даже на прототип самолета. И нарушение SRP в данном случае явно приводило к тому, что при изменении конструкции шасси, код в контроллере двигателя нужно было модифицировать и заново проходить сертификацию. Хуже того, нарушение этого принципа чуть было не стоило самолета и жизни пилота. К счастью наше обыденное программирование не грозит такими последствиями, однако пренебрегать принципами написания хорошего кода все равно не стоит. И вот почему:

  1. Декомпозиция кода уменьшает его сложность. К примеру если решение задачи требует от вас написать код c цикломатической сложностью равной четырем, то метод несущий ответственность за решение двух таких задач одновременно потребует кода со сложностью 16. Если же это разделить на два метода, то суммарная сложность будет 8. Конечно это не всегда сводится к сумме против произведения, однако тенденция будет примерно такая в любом случае.
  2. Unit-тестирование декомпозированого кода упрощается и становится более эффективным.
  3. Декомпозированый код создает меньше сопротивления изменениям. При внесении изменений меньше вероятность внести ошибку.
  4. Код становится лучше структурирован. Искать что-то в коде разложеном по файлам и папкам намного легче чем в одной большой портянке.
  5. Отделение boilerplate кода от бизнес логики приводит к тому, что в проекте можно применить кодогенерацию.

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

Существующие определения не работают


Одно из определений звучит так: «должна быть только одна причина, для изменения кода (класса или функции)». Проблема этого определения в том, что оно конфликтует с Open-Close принципом, вторым из группы принципов SOLID. Его определение: «код должен быть открыт для расширения и закрыт для изменения». Одна причина для изменения против полного запрета на изменения. Если подробнее раскрыть то что тут подразумевается, то окажется, что между принципами конфликта нет, однако между нечеткими определениями конфликт определенно есть.

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

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

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

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

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

Более подходящее определение


Из проб и ошибок у меня родилось определение получше:
Ответственность кода не должна быть слишком большой

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

Но как ее померить, как определить что ответственность данного кода слишком большая?

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

  1. Метрика cyclomatic complexity. К сожалению есть способы эту метрику маскировать, однако если вы ее будете собирать, то есть вероятность, что она покажет самые уязвимые места вашего приложения.
  2. Размер функций и классов. Функцию из 800 строк не нужно читать, чтобы понять, что с ней что то не так.
  3. Много импортов. Однажды я открыл файл в проекте соседней команды и увидел целый экран импортов, нажал page down и опять на экране были только импорты. Только после второго нажатия я увидел начало кода. Вы можете сказать, что все современные IDE умеют скрывать импорты под «плюсик», я же говорю, что хороший код не нуждается в сокрытии «запахов». Кроме этого, мне понадобилось переиспользовать небольшой кусочек кода и я вынес его из этого файла в другой, и за этим кусочком переехала четверть, а то и треть импортов. Этому коду явно было там не место.
  4. Модульные тесты. Если у вас все еще есть трудности с определением размера ответственности, заставьте себя написать тесты. Если на основное назначение функции нужно написать два десятка тестов, не считая пограничных случаев и т.д., значит нужна декомпозиция.
  5. То же относится к слишком большому числу подготовительных действий в начале теста и проверкам в конце. В интернете, кстати, можно встретить утопическое утверждение, что т.н. assert в тесте вообще должен быть только один. Я же считаю, что любая сколь угодно хорошая идея, будучи возведенной в абсолют, может стать до абсурдного непрактичной.
  6. Бизнес логика не должна напрямую зависеть от внешних инструментов. Драйвер Oracle, роуты Express-а, все это желательно отделить от бизнес логики и/или спрятать за интерфейсами.

Пара моментов:

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

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

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

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

Отделение бизнес логики от boilerplate кода


Разговаривая о правилах хорошего кода нельзя не обойтись без примеров. Первый пример посвящен отделению boilerplate кода.



Этот пример демонстрирует то, как обычно пишут back-end код. Люди обычно пишут логику неотрывно от кода указывающего Web-серверу Express такие параметры как URL, метод запроса и т.д.

Зеленым маркером я обозначил собственно бизнес-логику, а красным — инородное вкрапление кода, взаимодействующего с параметрами запроса (query string).

Я же всегда разделяю эти две ответственности таким образом:



В этом примере все взаимодействие с Express вынесено в отдельный файл.

На первый взгляд может показаться, что второй пример не принес улучшений, стало 2 файла вместо одного, появились дополнительные строчки, которых до этого не было — имя класса и сигнатура метода. И что же тогда такое разделение кода дает? В первую очередь — «точка входа приложения» теперь не Express. Теперь это обычная Typescript функция. Или javascript функция, ли C#, кто на чем пишет WebAPI.

Это в свою очередь позволяет совершать различные действия, недоступные в первом примере. Например вы можете писать behavior-тесты без необходимости поднимать Express, без использования http запросов внутри теста. И даже нет необходимости производить какое либо мокирование, подменять Router объект своим «тестовым» объектом, теперь код приложения можно просто вызвать из теста напрямую.

Еще одна интересная возможность, которую дает такая декомпозиция — теперь можно написать генератор кода, который будет парсить userApiService и на его основе генерировать код, связующий этот сервис с Express. В своих будущих публикациях я планирую обозначить следующее: кодогенерация не сбережет время в процессе написания кода. Затраты на кодогенератор не окупятся тем, что теперь не нужно копипастить этот boilerplate. Кодогенерация окупится тем, что код ей произведенный не нуждается в поддержке, что сэкономит время и главное — нервы разработчиков в долговременной перспективе.

Divide and conquer


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

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

  1. Данные нужно взять из БД
  2. Преобразовать в нужный формат
  3. Отправить получившийся отчет

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

async function sendEmployeeReportToProvider(reportId){
    const data = await dal.getEmployeeReportData(reportId);​
    const formatted = reportDataService.prepareEmployeeReport(data);​
    await networkService.sendReport(formatted);​
}

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

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

function prepareEmployeeReport(reportData){ 
    const salarySection = prepareSalarySection(reportData);​
    const workHoursSection = prepareWorkHoursSection(reportData);​
    return { salarySection, workHoursSection };​
}

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

Взаимодействие с Open-Close принципом


Вначале статьи я рассказал, что определения принципов SRP и Open-Close противоречат друг другу. Первый говорит, что должна быть одна причина для изменения, второй говорит, что код должен быть закрыт для изменения. А сами принципы, не только не противоречат друг другу, наоборот, они работают в синергии друг с другом. Все 5 принципов SOLID направлены на одну благую цель — указать разработчику какой код «плохой», и как можно его поменять чтобы он стал «хороший». Ирония — я только что подменил 5 ответственностей на одну ответсвенность побольше.
Итак, в дополнение к предыдущему примеру с отсылкой отчета в страховую компанию, представим, что к нам приходит бизнес аналитик и говорит, что теперь нужно добавить вторую функциональность в проект. Этот же отчет нужно выводить на печать.
Представим, что нашелся разработчик, который считает, что SRP «не про декомпозицию».
Соответственно ему этот принцип не указал на на необходимость декомпозиции, и он реализовал всю первую задачу в одной функции. После того, как ему пришла задача, он, объединяет две отвественности в одну, т.к. между ними много общего и обобщает ее название. Теперь эта ответственность называется «обслужить отчет». Реализация этого выглядит примерно так:
async function serveEmployeeReportToProvider(reportId, serveMethod){
    /*
    lots of code to read and convert the report
    */
    switch(serveMethod) {
      case sendToProvider:
        /* implementation of sending */
      case print:
        /* implementation of printing */
      default:
        throw;
    }
}

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

И как решает эту же задачу метод «divide and conquer»:
async function printEmployeeReport(reportId){
    const data = await dal.getEmployeeReportData(reportId);​
    const formatted = reportDataService.prepareEmployeeReport(data);​
    await printService.printReport(formatted);​
}

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

Альтернатива


Также я хотел упомянуть альтернативный, конкурирующий способ получать хорошо декомпозированный код, который выглядит примерно так — сначала пишем код «в лоб», затем рефакторим его используя различные приемы, например по книге Фаулера «Рефакторинг». Эти методы напомнили мне математический подход к игре в шахматы, где вы не понимаете что именно вы делаете с точки зрения стратегии, вы лишь вычисляете «вес» вашей позиции и пытаетесь максимизировать его делая ходы. Мне этот подход не нравился по одной небольшой причине — именовать методы и переменные и без того сложно, а когда у них нет бизнес-значения это становится невозможным. К примеру если эти методики подсказывают, что нужно выделить 6 одинаковых строк отсюда и оттуда, то выделив их, как назвать этот метод? someSixIdenticalLines()?
Хочу оговориться — я не считаю этот метод плохим, я лишь не смог приучиться его использовать.

Итого


В следовании приципу можно найти выгоду.

Определение «должна быть одна ответственность» — не работает.

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

Подход «divide and conquer» позволит сразу писать хорошо структурированый и самодокументированый код.

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 81

    +7
    «должна быть только одна причина, для изменения кода (класса или функции)»

    Историческое, но запутывающее определение.


    «у кода должна быть только одна ответственность»

    Нет, это определение совсем неправильное.


    Читаем дядюшку Боба

    Of all the SOLID principles, the Single Responsibility Principle (SRP) might be the least well understood. That’s likely because it has a particularly inappropriate name. It is too easy for programmers to hear the name and then assume that it means that every module should do just one thing.


    Make no mistake, there is a principle like that. A function should do one, and only one, thing. We use that principle when we are refactoring large functions into smaller functions; we use it at the lowest levels. But it is not one of the SOLID principles—it is not the SRP.


    Historically, the SRP has been described this way:


    A module should have one, and only one, reason to change.

    Software systems are changed to satisfy users and stakeholders; those users and stakeholders are the “reason to change” that the principle is talking about. Indeed, we can rephrase the principle to say this:


    A module should be responsible to one, and only one, user or stakeholder.

    Unfortunately, the words “user” and “stakeholder” aren’t really the right words to use here. There will likely be more than one user or stakeholder who wants the system changed in the same way. Instead, we’re really referring to a group—one or more people who require that change. We’ll refer to that group as an actor.


    Thus the final version of the SRP is:


    A module should be responsible to one, and only one, actor.

    Т.е. вот это вот всё "функция должна делать одну вещь и делать ее хорошо" – это полезно, но это не про SRP. SRP – это про то, что модуль должен зависеть от хотелок только одного актора (пользователя или стейкхолдера).


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

      –5
      функция должна делать одну вещь и делать ее хорошо
      И это определение не работает точно также, по той же причине. Не получится достоверно определить когда «вещь» одна, а когда нет. И как результат — ваш код изобилует т.н. code smells, а вы избегаете декомпозиции, прикрываясь тем, что якобы функция делает одну вещь, контролирует космическую станцию, ну или отправляя отчет в страховую компанию.
      но это из разряда: «Нормально делай – нормально будет».
      это из разряда: complexity kills, decompose it and you win.
        +9

        Т.е. "функция должна делать одну вещь" – это слишком расплывчато, а "Ответственность кода не должна быть слишком большой" – норм?


        якобы функция делает одну вещь, контролирует космическую станцию

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


        Ну и повторюсь, декомпозиция и разбиение на подзадачи – это хорошо, но к SRP никакого отношения не имеет.

          –1
          Т.е. «функция должна делать одну вещь» – это слишком расплывчато, а «Ответственность кода не должна быть слишком большой» – норм?

          Первое не расплывчато, оно не работает вообще.
          Второе работает примерно так — отправить отчет провайдеру — слишком большая ответственность. Считать данные(1), преобразовать данные(2) и отправить объект(3) в сеть — три ответственности поменьше. Суммарно они равны первой, слишком большой ответственности.
          Кроме того, что люди реализуют эти три ответственности в одном методе, они еще и «переплетают» их. То есть считав одну строку или число из базы, сразу стремятся преобразовать во что то. Тут и появляется помножение сложностей двух задач.

          Ну и повторюсь, декомпозиция и разбиение на подзадачи – это хорошо, но к SRP никакого отношения не имеет.
          Разбиение одной большой задачи на три маленьких это тоже самое что разбиение одной большой ответственности на три маленьких. Если вам запретить использовать большую ответственность для отговорок, и настоять на том, что ваш метод таки имеет три ответственности, то принцип SRP тут именно и при чём. Именно он скажет, что ответственность должна быть одна, следовательно метод надо разбить на три части.
            0
            Второе работает примерно так — отправить отчет провайдеру — слишком большая ответственность. Считать данные(1), преобразовать данные(2) и отправить объект(3) в сеть — три ответственности поменьше. Суммарно они равны первой, слишком большой ответственности.

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


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

            Ну нет же, не "ответственность должна быть одна", а "ответственность должна быть перед одним стейкхолдером".

              0
              ответственность должна быть перед одним стейкхолдером".
              и если стейкхолдер всего один, то весь код можно написать в одной функции.
                +1

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


                Во-вторых, стейкхолдер (а точнее, все-таки актор) – это не человек, а роль, и даже в небольшой компании один человек может быть несколькими акторами.


                В-третьих, да, если актор все-таки один, то SRP вам не предлагает разбивать этот модуль. Но:


                • модуль – это не обязательно функция/класс;
                • SRP не запрещает вам пользоваться другими принципами (и здравым смыслом), чтобы структурировать этот модуль.
                  0
                  Эти принципы созданы не «для приложений». Эти принципы созданы для разработчиков, для того, чтобы они писали «хороший» код и не писали плохой. Причина, зачем вообще нужен SRP — при изменении кода по просьбе одного «актора», если этот код также делает что то для другого актора — появляется вероятность внести ошибку. Если перейти в плоскость ответственностей, там происходит тоже самое. Если в методе переплетены две ответственности, то меняя одну из них, появляется вероятность сломать другую.
                  Уже на этой точке должно быть видно, что это один и тот же принцип.
                  Все принципы, так или иначе, направлены на единственную благородную цель — снизить количество ошибок. Снижать цикломатическую сложность нужно, потому что чем больше сложность, тем больше вероятность внести ошибку. Повышать тестируемость кода нужно, чтобы снизить вероятность ошибок. И так далее.

                  И да, я в статье упоминал про таких людей, которые говорят «этот принцип тут не применим». Это неверный ответ.
                    –1
                    Все принципы, так или иначе, направлены на единственную благородную цель — снизить количество ошибок.

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

                  0
                  и если стейкхолдер всего один, то весь код можно написать в одной функции.

                  И не просто можно — а нужно!
                  Зачем усложнять код, разбивая его на куски, если он хорошо выражается в рамках одной функции? Чтобы запутать потом читателя и усложнить сопровождение, тем самым?

                    +1
                    Затем, что это не единственная причина, по которой стоит декомпозировать функцию. Как раз чтобы облегчить восприятие и упростить сопровождение. Общую логику объемного кода проще понять, если он разбит на именованные абстракции, этими абстракциями проще управлять, повторяющиеся куски сложнее поддерживать и тд.
                      –1
                      Общую логику объемного кода проще понять, если он разбит на именованные абстракции

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


                      повторяющиеся куски сложнее поддерживать и тд.

                      Опять же — не всегда, есть "ложная копипаста", выделение которой только повышает затраты.

                        +1
                        Естественно важен, никто не предлагает делать рефакторинг ради рефакторинга. Речь идет о том, что «если стейкхолдер всего один» — это еще не повод всё паковать в одну функцию, есть и достаточное количество других причин.
                          +1
                          Так принцип SRP который описывает стейкхолдеров и не утверждает этого. Этот принцип работает только в одну сторону… А именно если у вас стейкхолдеров больше одного, то стоит иметь для них раздельные реализации… Кстати, это актуально даже если они дублируют друг друга. Класический пример что если у вас в компании два отдела в настоящее время считают ЗП одинаково(например использую ГРОСС), то при изменении способа подсчета в одном отеделе, не должно поведение поменяться для всех.

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

                            И не просто можно — а нужно!
                    0

                    Крайне редко бывает, что стейкхоллер, вернее актор, один физически. И даже если так, то, чаще всего он выступает в нескольких ролях. Вот по ролям и разделять. И помнить, что разработчик — это тоже роль, с одной стороны, а, с другой, запреты на изменения модуля они о публичном его контракте, прежде всего, о логике соответствующего уровня. Рефакторинги, оптимизации не входят, по-моему, в SRP и OCP. Если DBA жалуется на явную проблему 1+N запроса, то не надо отправлять его к главбуху, чтобы тот создал задачу на джойн запрос, а не вызов второго запроса в цикле по результатам первого.

                  0
                  И про «одну причину для изменения» все тоже также. Есть причина — надо поменять способ хранения данных в базе. Вторая причина — надо поменять протокол связи по сети. Если же ответственность метода воспринимать как «отправлять отчет», то обе эти причины звучат как нужно поменять отправление отчета, а остальное это детали. Акторы, юзеры, стейкхолдеры, как это все ни назови, это все об одном и том же. И проблема у всех определений одна и та же. И есть еще такой принцип в Domain Driven Design, называется Domain Distillation. Другое название, другой «уровень», а принцип тот же — декомпозируй.
              +1
              должен зависеть от хотелок только одного актора (пользователя или стейкхолдера).

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

                0
                Пересечения и взаимопротивоположенные хотелки — встречаются сплошь и рядом. Если хотите противоречивые функции изолировать — придется дублировать.

                Именно. SRP о том и говорит, что каждому актору – свой модуль. Какое-то перекрытие логики при этом возможно, и тут 2 пути: дублирование и вынесение общей логики в shared модуль. Что именно выбрать – it depends. Не всегда дублирование – это однозначное зло, в обоих случаях есть преимущества и недостатки.


                Деление на мелкие части не во всех случаях поможет.

                Абсолютно согласен. Более того, я на протяжении всей это ветки говорю о том, что деление на мелкие части (декомпозиция) не имеет никакого отношения к SRP.


                Зачастую лучше наплевать на этот принцип и согласовать хотелки между собой.

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

                  0
                  Сегодня вы их согласовали, а завтра опять у каждого актора свои потребности.
                  У нас есть один такой проект с двумя «акторами». Однажды заказчики пришли и сказали — а давайте вы стартуете еще один проект на той же кодовой базе, используя наработки. Плюс часть страниц очень похожа, а то и полностью совпадает. И наши ребята стартовали это. Помучавшись какое то время от того, что разработчик одного проекта что то ломает в другом, естественно разработчик не будет запускать «чужой проект», чтобы проверить, их «разделили», потом разделили побольше, потом еще больше, в результате это стали две папочки с полностью непересекающимся кодом. А там где до сих пор не разделили, сплошь и рядом встречаются строчки if(isSecondProject). Вот именно что то такое вы считаете имелось ввиду принципом SRP?
                  Я считаю, что их надо декомпозировать еще на уровень выше. Не раскладывать в две папки в одном проекте, а разложить в два отдельных проекта. И да, SRP все еще про декомпозицию, хоть и на другом уровне. И даже в этом, абсурдном, не дающем абсолютно ничего разработчику, понимании, вопрос все равно в отделении одного от другого, мух от котлет.
                    0

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


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


                    В DDD есть понятие Bounded Context, и даже в рамках одного проекта, например, сущность Customer может находиться в разных контекстах, с частичным дублированием логики – в случае, когда над разными контекстами работают разные команды, к shared коду надо относиться с большой опаской. И да, если вы что-то вынесли в общий код – то это становится общей ответственностью, иначе получается вот это "естественно разработчик не будет запускать «чужой проект», чтобы проверить".


                    Мне очень не нравится то, что большинство программистов настолько запуганы этим DRY, что у них любая строчка кода, встретившаяся в проекте больше одного раза, вызывает паническую атаку. Только вот DRY не об этом. 2 функции с абсолютно разными внутренностями могут нарушить DRY, и в то же время 2 функции с абсолютно одинаковым телом могут никак под него не попадать. И вот как раз "сплошь и рядом встречаются строчки if(isSecondProject)" – это и есть самое настоящее нарушение DRY.


                    Беда в том, что (как и в случае с SRP) само название DRY не передает сути принципа (а скорее даже запутывает кажущейся простотой). Но тут надо помнить, что это просто "label" для принципа, название и не должно быть исчерпывающим. Тут либо разбираться в том, что стоит за этими принципами, либо вовсе не прикрываться ими для защиты неудачного решения.

              +9
              Определение «должна быть одна ответственность» прекрасно работает.
              Просто всегда надо помнить, что одна ответственность определяет не только задачу, но и уровень на котором ответственность реализуется.
              Если у нас есть ферма и у неё одна ответственность — растить животных, и она не знает ничего о существовании куриных яиц то всё ок.
              Если у нас есть ферма и у неё одна ответственность — растить животных, но при этом она еще занимается менеджментом куриных яиц, значит что-то пошло не так.

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

              Поэтому «одна ответственность + работа только на одном уровне иерархии», и никакий эмпирические оценки не нужны.
                0
                Да. Очень грамотно и по сути. Одна ответственность на одном (соответствующем ей) уровне.
                Один выход, входов может быть много — но все на одном общем для них уровне, всего на один ниже уровня выхода.
                +12
                Что я только что прочитал? «принцип единственной ответственности», Angular, React, это уже какая то графомания от кода, написать статью состоящую на 99% из воды, штампов, бреда, отсылок к принципам ООП, цитат из книг, скрама, аджайла. Что вы хотели вообще всем этим сказать? Как написать функцию из четырех строк и не наделать ошибок? О чем это все?
                  0
                  НЛО прилетело, и опубликовало эту запись здесь
                  Честно говоря хотел пожаловаться на малое количество примеров, но потом перечитал пост, и понял что примеров достаточно.
                  –6
                  Также я хотел упомянуть альтернативный, конкурирующий способ получать хорошо декомпозированный код, который выглядит примерно так — сначала пишем код «в лоб», затем рефакторим его используя различные приемы, например по книге Фаулера «Рефакторинг». Эти методы напомнили мне математический подход к игре в шахматы, где вы не понимаете что именно вы делаете с точки зрения стратегии, вы лишь вычисляете «вес» вашей позиции и пытаетесь максимизировать его делая ходы. Мне этот подход не нравился по одной небольшой причине — именовать методы и переменные и без того сложно, а когда у них нет бизнес-значения это становится невозможным. К примеру если эти методики подсказывают, что нужно выделить 6 одинаковых строк отсюда и оттуда, то выделив их, как назвать этот метод? someSixIdenticalLines()?


                  Чем только не занимаются, лишь бы не работать. Специальная книга по рефакторингу — что, простите? Вам больше почитать что ли нечего? А книги по Hello World ещё не написали?
                    +2
                    +1
                    что то из серии «как не дописать программу которая нихрена не делает, с использованием машинного обучения, блокчейна, тайпскрипта, аджайла и скрама, применяя все фрэймворки и паттерны вычитанные в книжках, покрыть ее тестами на 300% и попутно написав об этом несколько тонн статей»
                      +3
                      Зря Вы так, нетривиальная задача, если подумать
                        0
                        Нетривиальная. Но никому не нужная. Идеальный код за «дорого» и «долго» не интересует вообще никого, даже НАСА (им тоже достаточно рабочего). Более того, без практических метрик (например, полезности для бизнеса) идеальность кода становится сферической в вакууме и может принимать любые формы, в том числе и чудовищные.
                          0

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

                            0
                            Вы чрезмерно упрощаете. Жизнь чуть сложнее простых фраз а-ля «давайте писать хорошо и фичи будут быстро добавляться». Во-первых, можно писать сколь угодно хорошо, и не запланировать расширения в некоторую сторону, которая потом внезапно станет очень нужна. И наоборот, можно говнокодить 24/7, но сделать расширяемость именно там, где далее нужно.

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

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

                                0
                                хороший код более предсказуем (*и для бизнеса тоже)

                                Да, с такой формулировкой я очень даже согласен.
                                0
                                Мне кажется не стоит недооценивать говнокод. Вот я копаю проект, с которого меня местами бомбит. Базовый класс сервис, метод Read, внутри вызывает Write. В другом месте снаружи приходит флаг isShallow и передается в следующий метод, принимающий флаг isDeep, не инвертируя.
                                Ну это так, по мелочи.
                        +8
                        Забавно, я думал нет таких разработчиков, которые не знают про эту книгу, и тем более утверждающих, что рефакторинг это что-то настолько тривиальное, что это можно сравнить с написанием Hello World.
                        –2

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

                          +1
                          Принцип Open-Close вполне чёткий. Если при добавлении новой функциональности пришлось поменять существующий код — принцип нарушен. Причем он нарушен был раньше, сам факт изменения существующего кода лишь сигнализирует о нарушении.
                            +1
                            Принцип четкий, спору нет, но есть проблемы. Первая — никаких границ применимости нет. То есть непонятно, нужно ли все 100% кода делать расширяемыми во все стороны или только часть. Если часть, то значит есть некие требования, данные свыше, о том, что это часть должна быть расширяемой. Но раз есть требования к расширяемости, из которых эта расширяемость вытекает явно, то зачем принцип?

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

                            Третья — противоречия с другими, не менее священными принципам. Очень общие сигнатуры вроде
                            interface IFunction<TInput, TOutput> { TOutput Run(TInput); }
                            на практике всегда противоречит LSP. Усложнение системы в угоду расширяемости противоречит KISS. Попытка спрятать все за абстракциями натыкается на принцип текущих абстракций Джоэла Спольски и т.д.
                          –1
                          Декомпозиция кода уменьшает его сложность. К примеру если решение задачи требует от вас написать код c цикломатической сложностью равной четырем, то метод несущий ответственность за решение двух таких задач одновременно потребует кода со сложностью 16. Если же это разделить на два метода, то суммарная сложность будет 8. Конечно это не всегда сводится к сумме против произведения, однако тенденция будет примерно такая в любом случае.

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


                          Добавляем новую функцию.

                          Зачем? Она же нигде не вызывается, лучше ее удалить.

                            +1

                            Где она возросла? Это совсем не очевидно.

                              –2
                              Где она возросла?

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

                                +3
                                Позвольте вмешаюсь
                                повырезайте случайные куски, сделав их параметрами, дайте случайные идентификаторы в качестве имен и сделайте сноски один на другой

                                Во-первых, идентификаторы выдаются вовсе не случайно, мы всегда стараемся давать классам, методам, переменным осмысленные названия, это делает код легкочитаемым и самодокументированным.
                                Во-вторых, пример приведенный вами — это любой юридический текст, будь то договор, закон или другой нормативный акт. Представьте если бы в договоре каждое упоминание стороны не было бы заменено простым и понятным «ПОСТАВЩИК», а каждый раз цитировалось бы полностью вместе с представителем и основанием полномочий. Неужели такой договор было бы легче читать? А редактировать?
                                В-третьих, я соглашусь, что не любой подвергнутый декомпозиции код будет легко прочитать, однако, если это сделано правильно, его будет намного проще переиспользовать или расширить.
                                  –2
                                  Во-первых, идентификаторы выдаются вовсе не случайно, мы всегда стараемся давать классам, методам, переменным осмысленные названия, это делает код легкочитаемым и самодокументированным.

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


                                  Надо же понимать какие именно действия приводят к каким эффектам.


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


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

                                    0
                                    Всё же обычно имеется в виду нормальное именование выделяемых програмных сущностей, а не обфускация. Сделав из функции с телом, допустим, 25 строк, три функции по 7-10 строк с именами, описывающими суть эти функций, а начальную превратив в вызов этих трёх функций, мы получаем возможность понять её за несколько секунд, буквально одним взглядом. Комментарии, аналогичные именам функций похожи на выделения, но возможности понять что происходит, где надо вносить изменения с одного взгляда, они не дают. С ними всегда надо читать код полностью, они помогают понять и помогают навигации в уже понятом, но, увы, понять с первого взгляда возможность если и дают, то при строгой уверенности, что эти комментарии всегда есть, что можно настроить редактор так, чтобы код между такими комментариями он сворачивал по умолчанию.
                                      0
                                      Всё же обычно имеется в виду нормальное именование выделяемых програмных сущностей, а не обфускация.

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


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

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

                                        +1
                                        Да, декомпозиция — это просто разбиение. Но кроме этого разбиения есть другие ортогональные декомпозиции требования к коду, которые должны выполняться всегда. Одно из них — человекочитаемые имена программных сущностей со строгим соответствие названия и содержания.

                                        На экране не одно и то же. В одном случае у нас три строки описывающие, что функция делает, подряд. А во втором условно те же строки, но не подряд, а с «шумом» в виде 7-9 строк, описывающих как функция это делает.
                                      0
                                      Так код становится легкочитаемым и самодокументированным не из-за того что вы порезали его на куски, декомпозировав, а из-за того, что вы дали читаемое и понятное пояснение этим кускам.

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

                                      Собственно человек только и делает, что запаковывает всё в абстракции для того, чтобы упростить понимание. Мы создаем различные объекты реального мира, конструкции, механизмы и даем этому всему лаконичные названия, даем названия действиям с этими объектами и тд.
                                        –1
                                        Как раз таки из-за того, что порезали и дали подходящие имена.

                                        Ну то есть не из-за декомпозиции. О чем я речь и веду.
                                        Декомпозиция восприятие текста усложнила, но пояснения — упростили.


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

                                        Но это все не имеет отношения к декомпозиции. Декомпозиция — это просто порезать на куски, все. Именования при декомпозиции вообще не предполагается.

                                          +2
                                          О чем я речь и веду.

                                          разве?
                                          не из-за того что вы порезали его

                                          Как раз таки из-за того, что порезали


                                          Далее
                                          Но это все не имеет отношения к декомпозиции

                                          Еще как имеет. Мы говорим «открыть дверь» и не приводим определений для слов «открыть» и «дверь». Сами определения вырезаны из выражения и заменены на абстрактные обозначения.

                                          Декомпозиция — это просто порезать на куски, все. Именования при декомпозиции вообще не предполагается.

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

                                            Ну так еще раз — порежьте что-то, перемешайте, и не давайте нормального нейминга кускам. Стало проще? Не стало.


                                            Еще как имеет.

                                            Какое же? Я взял, порезал на куски — вот декомпозиция. Декомпозиция даже не предполагает что кускам вообще даются какие-либо имена.


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

                                            Это не я употребляю, это стандартное значение данного термина.


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

                                            Так ведь именно так и происходит! Человек режет код невнятным образом, а потом говорит о том, что "ну я же декомпозировал, а декомпозиция — это хорошо!". Приводит какие-то абстрактные соображения, вроде цикломатической сложности или того, что "ни одной ф-и длиннее пяти строк!". Ну очевидно ведь, что цикломатическая сложность или длина ф-й с хорошими неймингами никак не связана? Значит, и показателем быть не может, разве нет? Но, между тем, эти показатели сплошь и рядом используются. Значит, люди не понимают, как оно устроено.
                                            Вот я, как бы, за то, чтобы понимать, что именно хорошо, а что — плохо.
                                            Резать код (что понимают чуть менее чем все под декомпозицией) — это плохо.
                                            Давать краткие пояснения в виде неймингов или при помощи какого-то другого способа (что, обычно, под декомпозицией никто не подразумевает) — хорошо.

                                              0
                                              Человек режет код невнятным образом, а потом говорит о том, что «ну я же декомпозировал, а декомпозиция — это хорошо!».

                                              Это заявление уровня «Ну оно же работает». Декомпозиция — это разделение кода не на произвольные куски, лишь бы только порубить, а разбиение на логические части. Это не «порезать и перемешать», это скорее можно сравнить с переработкой текста. Берем текст, написанный сплошняком, без знаков препинания, абзацев, переносов строк и т.д. И начинаем разбивать на предложения, абзацы, главы и части, логически связанные между собой. Вот это декомпозиция, а не просто произвольное «рубление и перемешивание».
                                                0
                                                Резать код (что понимают чуть менее чем все под декомпозицией) — это плохо.

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

                                                У декомпозиции есть цель, если цель «не допускать функции длиннее 5 строк», то проблема именно в цели. Как правило целью декомпозиции ставят что то адекватное, например: упростить понимание кода, избавиться от вредного дублирования и тп. Цель появляется не из неоткуда, разработчик сталкивается с проблемой или руководствуясь своим опытом видит, что она возникнет. Итого, наиболее типичный случай выглядит примерно так: разработчик обнаруживает проблему, для ее решения использует декомпозицию. Создание абстракций, нейминг — вечный спутник этого процесса. Под декомпозицией в программировании (да и не только) собственно и понимают вынос части чего то в какую то отдельную абстракцию.

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

                                                  Да вот же автор статьи цикломатическую сложность считает, далеко не надо ходить.

                                +3
                                Основательно проработал принципы SOLID

                                Я на днях получил оффер на .NET разработчика. На собеседовании меня 2 часа гоняли по базе, как устроен GC, генерейшны, как вызываются финализаторы, почему lock(this) {} плохо и т.д.

                                В конце они говорят, ну ок, а у тебя есть вопросы к нам?
                                Я говорю — вы меня только по внутренностям гоняли, почему не спрашивали по приниципам SOLID совсем?
                                Они отвечают — а мы никого не спрашиваем по ним, потому что они легко заучиваются.
                                ))
                                  0
                                  Обратная сторона SRP — это SPoF :)

                                  Например, когда речь идёт о надёжности, то более практично использовать избыточность(этому нас учит и авиация и биология и много чего ещё).

                                  Ни один из принципов не имеет никакой ценности сам по себе, в отрыве от контекста. Это основное правило, которое нужно знать и понимать. Остальное — дело наживное.
                                    +1
                                    Я бы сказал, больше похоже, что SPoF — обратная сторона принципа DRY. Именно он призывает во чтобы то ни стало избавляться от дублирования.

                                    SRP скорее наоборот. К примеру у нас была ситуация, где для разных пользователей была чуть разная логика проверки доступа к ресурсу. С точки зрения DRY — надо избежать дублирования. C точки зрения SRP — это две разные ответственности и им не стоит жить в одном методе.
                                    +1
                                    Спасибо за статью, полностью согласен с автором и с принципом SRP. Однако размышляя о SRP и ища в коде хорошие и наглядные примеры когда SRP помогает а когда усложняет, я наткнулся на класс List и у него полно методов и полный набор CRUD и кучу перегрузок по каждой букве из CRUD.
                                    И вот вопрос, нужно что делать отдельный класс структура данных и отдельные классы реализациий каждого метода?
                                    Это же будет дико не удобно:
                                    public class MyClass{
                                      public MyClass(IListAdder adder, IListRemover remover)
                                      public void MyMethod(IEnumerable<int> list){
                                           remover.Remove(10, list);
                                           adder.Add(12, list);
                                      }
                                    }
                                    
                                      +2
                                      класс List с методами CRUD в нем не нарушают SRP, ведь ответственность у всех методов одна — сам список
                                      А вот если в класс List добавить метод, который вычисляет, среднее время выполнения операций с списком, то это нарушение SRP. Наверно стоило бы создать какой-нибудь ListDiagnostics — его ответственностью будет статистика
                                      –1
                                      Сложность убивает, раздели ее на части и ты победишь
                                      но потреяешь системность. То есть поделия будут не системами, а кучками разрозненного мусора. Собрать в единое целое в результате, конечно же, удастся, но только только ценой удесятирения объёма кода.
                                        0
                                        Откуда удесятерение? Максимум удвоение, если каждую строку начального кода обернуть в отдельную функцию.
                                          –1
                                          Но системность от этого всё равно не появится. Будут только куча отдельных обёрнутых в функции строчек.
                                            0
                                            Системность как раз появляется при грамотной декомпозиции. Появление слоёв данных, представления, управления, инфраструктуры и т. п. — это следствие декомпозиции.
                                        0
                                        STP в моем понимании: класс существует и должен быть создан только для одной задачи.

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

                                        SRP распространяется и на компоненты, и на микросервисы.
                                          +2
                                          Лично я считаю, что SRP — это не про объем ответственности никоим образом. Это именно — у каждой единицы кода (модуль, класс, метод, функция) должна быть единственная ответственность. То есть когда мы делаем краткое описание оно не должно быть сложносочиненным. Например: «Модуль управляет космической станцией» — это нормально с точки зрения SRP. Вообще говоря, программа в целом решает какую-то задачу и, пока она сосредоточена на решении этой задачи, не пытаясь стать Jack-of-all-trades она тоже соответствует принципу SRP. Состоит эта программа из единственного огромного метода main() или разделена на 100500 трехстрочных функций — это детали её внутреннего устройства.

                                          Но в силу большого объема мы можем решить что модуль целесообразно разделить на подсистемы. Делаем мы это через делегирование ответственности нижележащим модулям. То есть задачу управления космической станцией разбиваем на подзадачи, например: «Поддержание ориентации в пространстве», «Корректировка орбиты», «Радиосвязь с ЦУП», «Отображение данных на приборах» и т.д., а в задаче верхнего уровня остается только управление модулями уровнем ниже. То есть, при выполнении такой декомпозиции, мы продолжаем следовать принципу SRP:
                                          а) Модуль для которого производится декомпозиция остается с единственной ответственностью — управление нижележащими модулями.
                                          б) Каждый из нижележащих модулей также соответствует SRP на своем уровне. Допустим модуль «Управление двигателями системы ориентации и сервоприводами солнечных батарей» очевидным образом обладает двумя ответственностями т.е. нарушает SRP.
                                          в) При такой декомпозиции у нас могут образовываться shared модули, которые используются несколькими подсистемами, например: «Модуль баллистических вычислений», которые также соответствуют SRP.

                                          В примере из статьи про отправку отчета: функция «отправки отчета по почте» вполне отвечает SRP. Но как только она превращается в функция «отправки отчета по почте или печати на принтере» — она перестает соответствовать SRP. Изменив функцию на «обработка запроса построения отчета» и реализовав как-то так:
                                          public void handleReportRequest(Request rq) {
                                            final ReportData data = DataSource.getReportData(rq.getReportId(), rq.getReportParams());
                                            if (rq.isSendByEmail()) {
                                              //отправить по почте
                                            }
                                            if (rq.isPrint()) {
                                              // напечатать на принтере
                                            }
                                          }
                                          

                                          мы снова в рамках SRP. Это нарушение OCP, но такая функция вполне укладывается в KISS и YAGNI. Продолжая усложнять систему, мы будем усложнять эту функцию пока не захотим провести декомпозицию, то есть делегировать ответственность другим функциям (и при декомпозиции снова будем следовать SRP).

                                          Также SRP это про то что «одна задача должна решаться одним модулем», это сильно коррелирует с DRY. Иначе получается что мы либо размазываем одну ответственность по всем модулям программы, либо (если есть выделенный модуль) — залезаем в его область ответственности.
                                          Например, у нас есть модуль Mailer для отправки почты. Если в handleReportRequest для отправки отчета мы используем его — все ОК. Если же мы сами начали там реализовывать взаимодействие с SMTP-сервером — этот тот самый случай залезания в чужую область ответственности и нарушение DRY.
                                          Другой возможный пример: когда у нас каждый модуль начинает проверять права доступа (то есть нет отдельного модуля, который отвечает за контроль доступа). Таким образом, ответственность «обеспечить разграничение прав доступа» оказывается размазана по всей программе. Опять же диагностировать эту ситуацию можно через отслеживание дублирования кода (DRY). Решением этой проблемы является выделение shared модуля, берущего данную ответственность на себя.
                                            0
                                            Какие-то сферические советы программиста в вакууме.
                                            По хорошему сначала собираются требования к решению задачи.
                                            Потом — требования к коду.
                                            Если это одноразовый скрипт — на фига вот это все.
                                            И вообще, к чему это может привести можно посмотреть на шуточных «обучающих» примерах как этот.
                                              0

                                              VladVR Владимир, спасибо за статью!
                                              Расскажите, пожалуйста, подробнее, что Вы всё-таки думаете даже не о Redux в частности, а о unidirectional data flow вообще? Аргументы "за" и "против", юз-кейсы, когда это стоит использовать, а когда — нет. Какие альтернативы? Очень интересно услышать Ваше мнение.

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

                                                  Меня этот подход интересует с точки зрения применения во фронтэнде несколько иного рода: в iOS и Android. Пока что мне нравится (ReSwift), но я ещё не наступил на все положенные грабли.


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


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


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


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

                                                    0
                                                    Правда, я встречался и с мнением, что UDF (именно в контексте статически типизированных языков) затрудняет прослеживание логики выполнения программы.
                                                    Не думаю, что есть какая либо разница с динамически типизированным языком.
                                                    Я с помощью генератора убираю action, action type и reducer от разработчика, и оно начинает выглядеть как вызоа конкретной функцией над узлом состояния. Примерно вот так
                                                    // функция изменяющая состояние
                                                    function disable(prev: MyState): MyState {
                                                      return { ...prev, isDisabled: true };
                                                    }
                                                    // потребитель
                                                    export class MyService {
                                                      constructor(dispatcher: MyStateDispatcher){}
                                                      public disableButtonPressed(): void {
                                                        this.dispatcher.disable();
                                                      }
                                                    }
                                                    

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

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

                                                      А зачем вам вообще в данном случае action, action type и reducer, если можно просто ограничиться классом MyStateDispatcher с методом disable, который обладает абсолютно всей необходимой информацией?

                                                        0

                                                        Есть такой момент, который вызывает у меня некоторые вопросы.


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


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


                                                        Следуя принципу SRP (который в конечном счёте сводится к римской максиме Divide et Impera), мы не хотим давать вью знать детали работы инфраструктурных адаптеров. И поэтому вместо их вызова напрямую общаемся через хранилище состояния. Это значит, что, скажем, API-адаптер тоже подписан на хранилище состояний и инициируется именно им. Когда запрос выполнен — он диспетчирует действие в духе TodoListReady(items: ...).


                                                        Возникает ситуация, когда поток "экшн" — "стэйт" — "экшн" — "стэйт"… начинает напоминать символ бесконечности, в центре которого находится хранилище состояний. То есть, разные компоненты, взаимодействующие с хранилищем, влияют друг на друга через него. Экшн от вью инициирует API, экшн от API приводит к изменению вью.


                                                        И тут есть выбор, кто должен знать обо всех этих экшенах и стэйтах.


                                                        Вариант 1. Вью диспетчирует экшэны, которые обновляют стэйт API-адаптера; адаптер диспетчирует экшены, которые обновляют стэйт вью. Вью и API-адаптер знают друг о друге (об экшенах и стэйтах друг друга).


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


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


                                                        Вот здесь, на мой взгляд, как раз и кроются различные трейд-оффы этой архитектуры. С чем удобнее работать, что проще отлаживать, какие специальные приёмы для документирования и отладки, особенно если это происходит в команде? Где в данном случае те самые точки опоры, которые позволят применить SRP по адресу и перевернуть Землю?


                                                        Основная проблема максимы Divide et Impera в мире IT заключается в том, что она была хорошо применима, когда составные части были географически и политически обособлены и имели high cohesion "из коробки". Таким образом, принцип работал на обеспечение low coupling. Однако, в программировании у нас нет такой роскоши, как high cohesion из коробки. И часто, добавляя желаемую обособленность составляющих частей, мы также и нарушаем cohesion, не желая этого. С этим мы боремся с помощью автоустаревающей документации, информации о типах, облегчающих перемещение в IDE, общения у маркерной доски...


                                                        Вот интересно, кстати, существует ли какой-то наглядный способ представлять эти action-state flow в виде диаграмм, который себя оправдывает в реальном проекте?

                                                          0
                                                          Возникает ситуация, когда поток «экшн» — «стэйт» — «экшн» — «стэйт»… начинает напоминать символ бесконечности
                                                          Это то, что фейсбук изначально предлагал избегать. здесь avoid cascading effects.

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

                                                          Как это все решаю я — вот нафантазировал пример
                                                          export class MyService {
                                                            constructor(dispatcher: MyStateDispatcher, api: MyApi){}
                                                            public async expandButtonPressed(): Promise<void> {
                                                              this.dispatcher.showProgressBar();
                                                              this.dispatcher.expandSection();
                                                              try {
                                                                const data = await api.getDetails();
                                                                if(someCondition) {
                                                                   this.dispatcher.showDetails(data);
                                                                } else {
                                                                   const otherDetails = await this.api.getOtherDetails();
                                                                   this.dispatcher.showOtherDetails(otherDetails);
                                                                }
                                                              } finally {
                                                                 this.dispatcher.hideProgressBar();
                                                              }
                                                            }
                                                          }
                                                          


                                                          вьюшка же делегирует нажатие кнопки этому методу.
                                                            0

                                                            В данном примере всё начинается с кнопки. То есть, пользователь инициирует обработку, нажимая кнопку. Вьюшка, вероятно, избавлена от ответственности диспетчирования экшенов — она дёргает делегат. А сама просто подписана на обновление состояния.


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


                                                            Некий сервисный слой координирует обработку событий от пользователя и взаимодействие с инфраструктурой.


                                                            В этом небольшом примере не видно, к сожалению, как решено взаимодействие с инфраструктурой, которое инициируется не пользователем, а, например, пришедшим push-сообщением, попаданием в определённую геозону или встряхиванием. Или если произошла ситуация, когда OAuth access token оказался недействительным, и от пользователя требуется выполнить повторный логин.


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

                                                    0
                                                    unidirectional data flow вообще? Аргументы "за" и "против", юз-кейсы, когда это стоит использовать, а когда — нет.

                                                    "unidirectional data flow вообще" уже лет так, наверное, 50 используется повсеместно и с успехом.

                                                      0

                                                      Druu, фротнэнд в том виде, в котором мы его сегодня знаем, появился не 50 лет назад. Меня интересует опыт применения этого подхода именно в контексте современного фронтэнда: веб и особенно iOS c Android-ом.


                                                      Речь идёт о том, что при реализации всегда можно выбрать между двухсторонним связыванием a-la AngularJS, всякими FRP-имплементациями вроде ReactiveX и подходом, который используют Elm, Redux и прочие. Вот именно причины этого выбора и сделанные выводы мне интересны.

                                                        0
                                                        Меня интересует опыт применения этого подхода именно в контексте современного фронтэнда

                                                        А чем современный фронтенд отличается в этом плане от любой другой области?

                                                          0

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


                                                          В любом случае — опыт интересен.

                                                            0
                                                            На мой взгляд, «подход Elm, Redux, и прочих» принципиально не так уж и отличается от двустороннего связывания и тому подобного. Это не чистый unidirectional data flow, в том смысле, что у вас всё равно есть способы взять и откуда-то из середины потока начать менять данные. Подходы Elm, Redux и прочих — они всего-то направлены на то, чтоб сделать эти способы максимально заметными. Не то, что все дёргают всех, а потом какой-нибудь несчастный программист хватается за голову в попытках разобраться, что у него с данными происходит, а то, что есть достаточно заметные места в коде, где запускается изменение данных. Ну и попутно конечно прочие сладкие плюшки, типа вменяемой архитектуры (что в случае Redux одновременно и достоинство и большой недостаток, например, потому что на любой чих появляется бойлерплейт-код), и тому подобное.

                                                            Я бы вообще сказал, что тут главное — это подход reactive programming в общем виде (потому что такой подход натурально позволяет выделить хранилище данных и более явно обозначить действия над ним, хоть называя их action, хоть еще как), а как конкретно это делать — это даже и не суть важно, вопрос только в том, сколько придётся додумывать самому или сколько придётся тратить усилий на впихивание своих задач в заранее придуманную архитектуру. А с технической стороны это вообще можно на голом es6 (Proxy) делать. Как собственно все библиотеки реактивного программирования и делают.
                                                    0
                                                    Единственную ответственность класса иногда путают с единственной ответственностью функции. SRP это не только для разделения больших абстракций (класс, пакет) на несколько меньших, но и про то что иногда не нужно абстракции делить. SRP всегда требует учета вектора изменения. Для примера.
                                                    У нас есть класс который забивает гвозди и рассчитывает фазу луны. По первому взгляду, мы сразу должны его разделить на два. Но если, вдруг, функции забивания гвоздей и расчет фазы обязательно связаны между собой, то делить нельзя. Т.е. если мы уверены, что изменения затрагивающие фазы, предполагают изменения забивания, деление класса на два приведет к тому, что мы вносить изменения не в один класс, а два постоянно. Так мы сделали сложность в поддержке своего кода. И к тому же нарушили KISS.

                                                      +1
                                                      А потом приходит заказчик и просит сделать следующие истории:
                                                      Забивать гвозди на марсе.
                                                      Рассчитывать фазы луны в штате США где запрещены гвозди.
                                                      От фаз луны должно зависеть выращивание картошки.
                                                      Надо чуть подправить рассчет фаз луны, но гвозди должны забиваться как и раньше.
                                                      и т.д.

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

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

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

                                                      Самое читаемое