ВЕРНУТЬСЯ К ОСНОВАМ

Секреты принципа единой ответственности

Раскройте секреты Принципа единой ответственности (SRP) Принципов SOLID.

Почти все разработчики, работающие с языками объектно-ориентированного программирования (ООП), знают о принципах SOLID.

ТВЕРДЫЕ принципы

  • Принцип единой ответственности
  • Oпринцип закрытой ручки
  • ЛисковПринцип подстановки
  • Принцип разделения интерфейса
  • DПринцип инверсии зависимостей

В этой статье мы собираемся объяснить S SOLID, принцип единой ответственности, однако то, что вы собираетесь прочитать здесь отличается…



Что такое принцип единой ответственности

Согласно Википедии:

Этот термин был введен Робертом С. Мартином в его статье Принципы ООП как часть его Принципов объектно-ориентированного проектирования, ставшей популярной благодаря его книге 2003 года Гибкая разработка программного обеспечения, Принципы, шаблоны и практика.

Мартин описал его как основанный на принципе сплоченности, описанном Томом ДеМарко в его книге Структурированный анализ и спецификация системы и Мейлиром Пейдж-Джонсом в Практическом руководстве по Проектирование структурированных систем.

В 2014 году Мартин опубликовал запись в блоге под названием «Принцип единственной ответственности» с целью разъяснить, что подразумевается под фразой «причина изменений».

И по его значению:

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

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

Итак, хватит разговоров о Википедии, давайте проясним некоторые моменты.

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

Люди могут запутаться в том, что подразумевается под причиной, и я полностью понимаю, откуда это взялось.

Позвольте мне уточнить…

Важная заметка

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

Путаница

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

Хорошо, но все же, что значит слишком много? Как узнать, делает ли класс слишком много вещей или нет? Если класс делает 2 вещи, это слишком много? А 3? А 4? …

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

Я слышу, как ты сейчас говоришь:

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

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

В программном обеспечении метод — или функция, как бы вы ее ни называли — имеет два фактора для описания:

  • Знания, необходимые для работы.
  • Достижение выполняется, когда оно завершено.

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

Теперь позвольте мне провести вас через это шаг за шагом.

Знание метода

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

Например, давайте посмотрим на этот метод:

public int Add(int a, int b)
{
  return a + b;
}

Метод Add должен уметь складывать два числа. Это делается с помощью оператора +, предоставляемого .NET Framework.

Теперь, если разработчики .NET Framework решат изменить внутреннюю реализацию оператора + или то, как он транслируется на уровне машинного кода, это не должно повлиять на метод Add, ему все равно. Он просто делегирует методологию добавления двух целых чисел в .NET Framework.

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

Итак, код должен быть следующим:

private const int Margin = 10;

public int Add(int a, int b)
{
  return a + b + Margin;
}

Теперь это другое.

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

Достижение метода

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

Например, давайте посмотрим на этот метод:

public class EmployeeManager
{
  private readonly EmployeeRepository _mEmployeeRepository;
  private readonly TaxCalculator _mTaxCalculator;
  private readonly MailingGroupsManager _mMailingGroupsManager;
  private readonly HolidaysCalculator _mHolidaysCalculator;

  public async Task<Employee> HireEmployee(Employee employee)
  {
    // Add Employee entry with basic info in DB
    var updatedEmployee = await _mEmployeeRepository.Add(employee);
    
    // Add Emplyee to mailing groups
    updatedEmployee = await _mMailingGroupsManager.Add(updatedEmployee);
    
    // Calculate tax scheme
    decimal tax = await _mTaxCalculator.Calculate(updatedEmployee);
    
    // Update tax scheme in DB
    updatedEmployee = await _mEmployeeRepository.UpdateTax(updatedEmployee, tax);
    
    // Calculate holidays
    byte holidaysCount = await _mHolidaysCalculator.Calculate(updatedEmployee);
    
    // Update holidays in DB
    return await _mEmployeeRepository.UpdateHolidays(updatedEmployee, holidaysCount);
  }
}

Как мы можем заметить здесь, объем работы, который будет выполнен после завершения этого метода, будет огромным. Однако можем ли мы сказать то же самое о знании? я так не думаю…

Единственное знание, которым обладает этот метод, — это знание шагов и порядка действий, необходимых для найма Employee, не более того.

Вы можете возразить, что метод знает больше, но на самом деле нет. Да, при запуске метода HireEmployee сотрудник будет добавлен в базу данных, но на самом деле он не знает, как это делается, именно метод Add класса EmployeeRepository владеет этими знаниями.

То же самое относится ко всем другим методам, и это делает метод HireEmployee проще, чем мы могли ожидать, прежде чем заметить это, верно?

Сказав это, давайте теперь проанализируем, как эти два фактора могут повлиять на определение принципа единой ответственности.

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

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

Позвольте мне уточнить больше…

Момент истины

Вы помните класс EmployeeManager, о котором мы говорили выше, скажем, у нас он реализован иначе.

Предположим, что код выглядит следующим образом:

public class EmployeeManager
{
  public async Task<Employee> HireEmployee(Employee employee)
  {
    // Add Employee entry with basic info in DB
    // Assume that here we are actually doing the following:
    // 1. Open a connection to the database
    // 2. Build a SQL query to insert records in the database
    // 3. Execute the query
    // 4. Close connection
    
    // Add Emplyee to mailing groups
    // Assume that here we are actually doing the following:
    // 1. Open a connection to the mailing server
    // 3. Do whatever needed to add records to the mailing groups
    // 4. Close connection
    
    // Calculate tax scheme
    // Assume that here we are actually doing the following:
    // 1. Do the mathematical operations required
    
    // Update tax scheme in DB
    // Assume that here we are actually doing the following:
    // 1. Open a connection to the database
    // 2. Build a SQL query to insert records in the database
    // 3. Execute the query
    // 4. Close connection
    
    // Calculate holidays
    // Assume that here we are actually doing the following:
    // 1. Do the mathematical operations required
    
    // Update holidays in DB
    // Assume that here we are actually doing the following:
    // 1. Open a connection to the database
    // 2. Build a SQL query to insert records in the database
    // 3. Execute the query
    // 4. Close connection
  }
}

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

Такого рода огромное знание является бременем, которое HireEmployee несет с собой метод.

Если новые изменения должны быть применены к таблице базы данных Employee или любой из связанных таблиц, метод HireEmployee необходимо будет обновить, а это означает, что класс EmployeeManager будет соответствующим образом обновлен.

Если новые изменения должны быть применены к почтовому серверу, метод HireEmployee необходимо будет обновить, а это означает, что класс EmployeeManager будет соответствующим образом обновлен.

И так далее…

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

Теперь я слышу, как ты говоришь:

Хорошо, теперь я понял, но все еще не знаю, как это исправить. Является ли это возможным?

Да, конечно, это возможно. Позволь мне показать тебе…

Лучший путь

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

Во-первых, нам нужно вернуться к лучшей, но еще не лучшей версии:

public class EmployeeManager
{
  private readonly EmployeeRepository _mEmployeeRepository;
  private readonly TaxCalculator _mTaxCalculator;
  private readonly MailingGroupsManager _mMailingGroupsManager;
  private readonly HolidaysCalculator _mHolidaysCalculator;

  public async Task<Employee> HireEmployee(Employee employee)
  {
    // Add Employee entry with basic info in DB
    var updatedEmployee = await _mEmployeeRepository.Add(employee);
    
    // Add Emplyee to mailing groups
    updatedEmployee = await _mMailingGroupsManager.Add(updatedEmployee);
    
    // Calculate tax scheme
    decimal tax = await _mTaxCalculator.Calculate(updatedEmployee);
    
    // Update tax scheme in DB
    updatedEmployee = await _mEmployeeRepository.UpdateTax(updatedEmployee, tax);
    
    // Calculate holidays
    byte holidaysCount = await _mHolidaysCalculator.Calculate(updatedEmployee);
    
    // Update holidays in DB
    return await _mEmployeeRepository.UpdateHolidays(updatedEmployee, holidaysCount);
  }
}

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

Однако, это лучшее, что мы можем сделать?

На самом деле нет, мы можем сделать лучше следующим образом:

public class TaxManager
{
  private readonly EmployeeRepository _mEmployeeRepository;
  private readonly TaxCalculator _mTaxCalculator;

  public async Task<Employee> UpdateTax(Employee)
  {
    // Calculate tax scheme
    decimal tax = await _mTaxCalculator.Calculate(updatedEmployee);
    
    // Update tax scheme in DB
    return await _mEmployeeRepository.UpdateTax(updatedEmployee, tax);
  }
}

public class HolidaysManager
{
  private readonly EmployeeRepository _mEmployeeRepository;
  private readonly HolidaysCalculator _mHolidaysCalculator;

  public async Task<Employee> UpdateHolidays(Employee)
  {
    // Calculate holidays
    byte holidaysCount = await _mHolidaysCalculator.Calculate(updatedEmployee);
    
    // Update holidays in DB
    return await _mEmployeeRepository.UpdateHolidays(updatedEmployee, holidaysCount);
  }
}

public class EmployeeManager
{
  private readonly EmployeeRepository _mEmployeeRepository;
  private readonly TaxManager _mTaxManager;
  private readonly MailingGroupsManager _mMailingGroupsManager;
  private readonly HolidaysManager _mHolidaysManager;

  public async Task<Employee> HireEmployee(Employee employee)
  {
    // Add Employee entry with basic info in DB
    var updatedEmployee = await _mEmployeeRepository.Add(employee);
    
    // Add Emplyee to mailing groups
    updatedEmployee = await _mMailingGroupsManager.Add(updatedEmployee);
    
    // Update tax scheme
    updatedEmployee = await _mTaxManager.UpdateTax(updatedEmployee);
    
    // Update holidays
    return await _mHolidaysManager.UpdateHolidays(updatedEmployee);
  }
}

Что мы сделали здесь:

  • Вынесены знания по расчету и добавлению налоговой схемы в отдельный класс TaxManager.
  • Выделил знания по расчету и добавлению праздников в отдельный класс HolidaysManager.

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

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

Теперь я слышу, как ты говоришь:

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

Да, я полностью согласен с вами, но я не вижу в этом проблемы.

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

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

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

Последние мысли

В этой статье мы обсудили принцип единой ответственности (сокращенно SRP) из принципов SOLID.

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

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

Наконец, я надеюсь, вам понравилось читать эту статью, как мне понравилось ее писать.

Надеюсь, вы нашли этот контент полезным. Если вы хотите поддержать:

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

Другие источники

Это другие ресурсы, которые могут оказаться полезными.











Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу