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

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

  • Если вы назначаете Джону новую задачу (продажа или разработка), вам необходимо убедиться, что она не мешает его другим запланированным задачам. Есть ли у него для этого подходящий временной интервал? Мог ли он спутать контракт API с контрактом купли-продажи? Мог ли он засорять задачу разработки соображениями продаж или наоборот?
  • когда Джон недоступен для работы (в отпуске, на учебе или по болезни), это влияет на все его задачи. Больше никаких разработок и больше никаких продаж.
  • Если вы хотите понять работу Джона, чтобы узнать, где включить новое бизнес-правило, вам придется просмотреть ряд различных вещей, которые могут не иметь отношения к вашему правилу, например отчеты о расходах, исходный код. , коммерческие предложения, архитектурные документы, контракты, тестовое покрытие и т. д.

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

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

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

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

Давайте снова возьмем пример с сотрудником-единорогом и спроектируем его так, как мы описали:

class Employee { 
  projects: Set<Project>
  contracts: Map<Contract, Client>
  constructor(vcs) {
    this.vcs = vcs
  }
   
  commit(proj, code) {
    this.vcs.commit(proj, code)
  }
   
  sign(contract, client) {
    this.contracts.put(contract, client)
  }
}

Похоже, сотрудник делает кучу разных вещей, не так ли? На самом деле новичку может быть сложно определить, какова цель этого класса… поскольку их много. Также может возникнуть путаница при чтении commit или sign API, которые можно интерпретировать как операции разработки или продажи.

Еще одна странность возникает, если мы хотим, например, выполнить пару торговых задач:

var vcs = new Git()
var john = new Employee(vcs)
var client1 = new InsurCorp()
var contract1 = new Contract()
john.sign(contract1, client1)

Зачем сотруднику VCS для подписания контракта? Если я хочу протестировать этот программный вызов, мне пришлось бы создать экземпляр VCS… бесплатно. Может быть, кто-то, не знакомый с аббревиатурами разработки, мог бы даже предположить, что VCS - это предмет продаж.

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

class Employee {
  projects: Set<Project>
  contracts: Map<Contract, Client>
  constructor(vcs) {
    this.vcs = vcs
  }
   
  commit(proj, code) {
    this.vcs.commit(proj, code)
  }
   
  sign(contract, client) {
    this.contracts.put(contract, client)
  }
  goVacation(backup: Employee) {
    backup.projects.add(this.projects)
    backup.contracts.putAll(this.contracts)
  }
}

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

Давайте реорганизуем это как два класса с разными обязанностями:

class Developer {
  projects: Set<Project>
  constructor(vcs) {
    this.vcs = vcs
  }
   
  commit(proj, code) {
    this.vcs.commit(proj, code)
  }
   
   goVacation(backup: Developer) {
      backup.projects.add(this.projects)
   }
}

Очевидно, этот класс проще понять, чем предыдущий. Неудивительно, к чему относится commit. Кроме того, я могу убедиться, что его проекты будут переданы другому Developer, который сможет с ними справиться.

Другой класс выглядит еще проще:

class Salesman {
  contracts: Map<Contract, Client>
     
  sign(contract, client) {
      this.contracts.put(contract, client)
   }
  goVacation(backup: Saleman) {
      backup.contracts.putAll(this.contracts)
  }
}

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

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

Этот рефакторинг следует принципу единой ответственности, изобретенному Робертом Мартином, также известным как« Роберт Мартин . Дядя Боб »и автор бестселлера Чистый код . В нем говорится, что:

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

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

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

Этот принцип способствует простоте, поскольку тем меньше у вас будет проблем:

  • проще будет код (т. е. обработка меньшего количества концепций и уменьшение размера кода, что сделает ваши показатели качества кода удовлетворительными);
  • меньше зависимостей, которые он будет иметь (что уменьшит взаимосвязь и упростит тестирование).

Примеры

История Джона была скорее метафорой, олицетворением программного обеспечения. Более конкретно, вот несколько примеров применения SRP.

Виджеты

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

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

Какая от этого польза?

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

Наслоение

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

Структура данных

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

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

interface Customer {
   id: UUID
   data: CustomerData
}

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

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

interface Customer {
   id?: UUID           // Database generated
   data: CustomerData
}

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

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

Функциональное программирование

Думаете, SRP применим только к объектно-ориентированному дизайну?

Функция для каждого действия

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

Расширяя это

Как мы упоминали ранее, как и все принципы, SRP следует применять на соответствующем уровне абстракции. Давайте посмотрим на другие уровни помимо объектов:

  • Функции и алгоритмы в целом также могут сочетать несколько задач или придерживаться одной. Например, см. случай необязательных параметров.
  • Хранение данных из нескольких, но разных концепций в одной и той же единице хранения (например, в одной строке таблицы) может означать некоторую нежелательную привязку (например, я не могу удалить одну концепцию, не удалив другую).
  • Именование неявно требует, чтобы вы четко указали их цель. Если вам сложно назвать что-то, скорее всего, это связано с тем, что оно служит слишком многим различным целям и не позволяет применять SRP.
  • Инкапсуляция - это принцип программирования, который помогает разделить публичные и частные (или защищенные) контракты. Публичный контракт часто изолируется определением интерфейса.
  • Исключения - это способ отделить деловой контракт от контракта на обработку ошибок, чтобы последний не загрязнял первый. Другой типичной ошибкой является смешивание контрактов на обработку ошибок и освобождение ресурсов.
  • UX также общеизвестно неэффективен при отображении или запросе слишком большого количества вещей одновременно и всегда должен фокусировать внимание пользователя на одной задаче, не отвлекаясь ни на что.
  • Разве в архитектуре не имеет смысла отвечать за данную услугу, и не более того? Это одна из идей, лежащих в основе микросервисов.
  • При использовании VCS вы не хотите обрабатывать несколько тем в коммите, потому что мы не сможем выбрать или отменить их индивидуально.
  • В тестировании также имеет смысл, чтобы каждый тест проверял только одну функцию, а не много (поэтому рекомендуется имитировать зависимости тестируемого объекта). Потому что, если он проверяет более одного, вы можете не знать, какой из них вышел из строя.
  • В демократии критически важно, чтобы правосудие, законодательная и исполнительная власть были независимы друг от друга (поскольку одно влияние на другое нанесло бы ущерб целям равенства и свободы), при этом сотрудничая для управления одной и той же страной.
  • В повседневных задачах обычно более эффективно сосредоточиться на одной задаче (и завершить ее), чем переключаться между ними (поскольку это позволяет избежать затрат на переключение контекста). Это не означает, что параллельная обработка менее эффективна, чем последовательная (на самом деле все наоборот), но что данная обработка (параллельная или нет) должна иметь дело только с одной задачей за раз.

Заключение

Если только один, то SRP должна быть принципом, которому должен следовать каждый разработчик, поскольку он продвигает:

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

* SRP не следует путать с Разделением проблем (SoC). Хотя последнее звучит лучше и даже более интуитивно близко к тому, что цель этой статьи выразить, это не предназначалось для этого. Когда он впервые сформулировал это в 1974 году, Дейкстра говорил о различных этапах разработки программного обеспечения и рекомендовал изолировать проблемы каждого из этих этапов (что ожидается, как это сделать, «ценно? ”И т. Д.) Друг от друга.