Чем проще код - тем меньше в нем остается мест для ошибок, и тем легче его сопровождать. Существует множество принципов программирования, которых вам следует придерживаться (с учетом обстоятельств), для создания более простого кода. Например, мы уже затрагивали принцип DRY, где говорили о вреде дублирования кода. Однако не менее полезным на практике оказывается принцип единой ответственности, о котором и пойдет речь.
Суть принципа достаточно очевидна и следует из его названия: переменная, функция или класс должны иметь одно строго определенное назначение и справляться с этим назначением хорошо. Интуитивно понятно, что если бессистемно начать размазывать функционал по коду приложения, то ничего хорошего не выйдет. Чтобы лучше понять идею, рассмотрим несколько примеров нарушения принципа.
Довольно часто встречается ситуация, когда одна и та же переменная используется сразу для нескольких целей. Например:
User user;
// Заполняем структуру user
int userID = registerUser( user );
if( userID == -1 ) {
// Регистрация не удалась!
}
Проблема этого кода в том, что userID
является одновременно и идентификатором вновь зарегистрированного пользователя, и кодом ошибки. Такой прием часто используется в C-библиотеках, однако он имеет множество недостатков:
registerUser()
и может привести к неправильной интерпретации возвращаемого значения;Наиболее очевидные способы устранения приведенного нарушения принципа:
Нагружаемый силовой тренажер для дома от производителя на фабрике.
Мы уже рассматривали этот пример, когда говорили о защитном программировании, поэтому за подробностями обращайтесь к указанной заметке.
Когда принцип единой ответственности нарушается для функций, то это приводит к множеству проблем:
processData()
или doWork()
, то имеет смысл еще раз подумать над ее назначением;Например, функция-член save()
следующем классе явно нарушает принцип единой ответственности:
class Table {
public:
// …
// Сохраняет данные таблицы на диск в формате CSV
void save() {
// Запрашивает имя файла у пользователя
// Если пользователь выбрал файл, то производит запись данных на диск
// Иначе ничего не делает
}
};
Кому-то может показаться удобным, чтобы имя файла запрашивалось прямо в функции сохранения. И до каких-то пор это может работать. Но при развитии проекта обязательно вылезет множество недостатков:
askForFileNameAndSave()
. Сразу видно, что функция делает два дела сразу;Исправить проблему довольно просто:
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 ); // Автомойка моет машину - ОК
};
Однако в реальных приложениях все может быть не так однозначно. Главное - придерживайтесь здравого смысла. В частности: не смешивайте разные уровни абстракции в рамках одного класса, и избегайте очевидных несоответствий.