IT Notes

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

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

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

Нарушение принципа единой ответственности для переменных

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

User user;
// Заполняем структуру user

int userID = registerUser( user );
if( userID == -1 ) {
    // Регистрация не удалась!
}

Проблема этого кода в том, что userID является одновременно и идентификатором вновь зарегистрированного пользователя, и кодом ошибки. Такой прием часто используется в C-библиотеках, однако он имеет множество недостатков:

  1. Без документации или комментариев нельзя с уверенностью определить смысл переменной, что осложняет использование функции registerUser() и может привести к неправильной интерпретации возвращаемого значения;
  2. Сделано допущение о том, что значение идентификатора не может быть меньше нуля. Однако это условие не контролируется компилятором, что может стать причиной опечаток и ошибок из-за невнимательности;
  3. Если потребуется передать более информативный код ошибки, то могут возникнуть проблемы с сопровождением существующего кода.

Наиболее очевидные способы устранения приведенного нарушения принципа:

  1. Использовать дополнительную переменную с кодом ошибки;
  2. Возбуждать исключение в случае ошибки.

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

Нарушение принципа единой ответственности для функций

Когда принцип единой ответственности нарушается для функций, то это приводит к множеству проблем:

  1. Функция получает побочные эффекты, поскольку делает сразу несколько логических действий одновременно. Это осложняет ее повторное использование и может привести к неожиданным последствиям, если побочные эффекты не были учтены;
  2. Функция имеет либо сложное имя, либо (что чаще всего и происходит) имя, которое вводит в заблуждение или ни о чем не говорит. Если вам сложно подобрать имя для функции, то эта функция с большой вероятностью нарушает принцип единой ответственности. Например, если в коде появилась функция processData() или doWork(), то имеет смысл еще раз подумать над ее назначением;
  3. Реализации функции оказывается сложной и запутанной. В большинстве случаев (хотя бывают исключения) если код функции не умещается на экране вашего монитора, то функцию можно разбить на несколько более простых.

Например, функция-член save() следующем классе явно нарушает принцип единой ответственности:

class Table {
public:
    // …
    // Сохраняет данные таблицы на диск в формате CSV
    void save() {
        // Запрашивает имя файла у пользователя
        // Если пользователь выбрал файл, то производит запись данных на диск
        // Иначе ничего не делает
    }
};

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

  1. Из-за неправильно выбранного уровня абстракции функция (и класс) зависит от графического пользовательского интерфейса. Перенести без изменений на другую платформу его нельзя;
  2. Имя функции вводит в заблуждение. Ее настоящее имя должно быть вроде: askForFileNameAndSave(). Сразу видно, что функция делает два дела сразу;
  3. Из-за побочного эффекта мы ограничены текущим вариантом использования функции. Например. если имя файла поступит откуда-то извне, а не от пользователя, то передать его в функцию нельзя.

Исправить проблему довольно просто:

class Table {
public:
    // …
    // Сохраняет данные таблицы на диск в формате CSV
    void save( const std::string& fileName ) {
        // Запись данных на диск в файл с именем fileName
    }
};

Нарушение принципа единой ответственности для классов

Те проблемы, которые мы выделили для переменных и функций в случае нарушения принципа единой ответственности, относятся и к классам. Типичными примерами классов с нарушением принципа являются различные разновидности Utils и Tools, которые могут делать все, что угодно. Нельзя сказать, что подобные классы являются чем-то совсем недопустимым, но явно указывают на то, что разработчику было некогда или лень найти подходящее место для функционала, который он в итоге туда "спихнул".

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

class Car {
public:
    void move(); // Машина двигается - ОК
    void wash(); // Машина моется - не понятно
};

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

class Car {
public:
    void move(); // Машина двигается - ОК
};

class CarWash {
public:
    void wash( Car* car ); // Автомойка моет машину - ОК
};

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

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