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

SПринцип единой ответственности (SRP) — это один из пяти принципов SOLID объектно-ориентированного программирования. Несмотря на простое определение, SRP может быть сложно применить в реальных условиях.

Формальное определение

Давайте посмотрим на более формальное определение SRP:

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

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

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

Обязанности

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

Проверить, есть ли у класса более одной обязанности, — непростая задача! Рассмотрим следующий пример –

class User {
  string name;
  int age;
  bool isAdmin;
public:
  virtual void registerUser() = 0;
  virtual void sendWelcomeEmail() = 0;
};

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

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

  1. Хранение информации, связанной с пользователем, такой как имя, возраст и статус администратора.
  2. Регистрация пользователей в базе данных.
  3. Отправка приветственного письма Пользователю.

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

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

class User {
  string name;
  int age;
  bool isAdmin;
public:
  virtual void getFirstName() = 0;
  virtual void getLastName() = 0;
  virtual void getName() = 0;
  virtual void getAge() = 0;
  virtual void isUserAdmin() = 0;
};

class UserRepository {
public:
  virtual User& registerUser(const User& user) = 0;
  virtual User& updateUserName(const User& user) = 0;
};

class UserService {
public:
  virtual void sendUserWelcomeEmail(const User& user) = 0;
  virtual void sendPromotionEmailToAllUsers() = 0;
};

Теперь у каждого класса есть одна и только одна обязанность!

Сплоченность против сцепления

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

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

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

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

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

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

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

Освоение этого принципа действительно может помочь любому разработчику на пути к созданию более качественного программного обеспечения!