OOP

Это мини-серия статей по написанию поддерживаемого объектно-ориентированного кода без лишней нервотрепки.

 

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

Управление побочными эффектами

Отличным способом управления побочными эффектами является создание четкого разделения команд и запросов. В данном контексте команда изменяет систему и обладает побочным эффектом. Запрос возвращает вычисленное значение или наблюдаемое состояние системы.

Пример

Вы ждете, что при вызове функции getAmount()будет возвращаться некое значение без изменения системы. И было бы ужасно, если бы все происходило наоборот. То же и с вызовом setAmount(, у которой есть свои побочные эффекты. Вы знаете, что она должна изменять состояние системы. Но что, по вашему мнению, вернет setAmount()? Вероятно, ничего.

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

Разделение команд и запросов дает формальное определение:

Функции, которые изменяют состояние, не должны возвращать значения, а функции, возвращающие значения, не должны изменять состояние.

Данный термин был придуман Бертраном Мейером в его книге «Объектно-ориентированное конструирование программных систем».

Плюсы

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

int m();  // query
void n(); // command

Исключения

Посмотрите на вызов ниже. Это команда или запрос? Очевидно, что такая строка изменяет состояние системы.

User u = UserService.login(username, password);

А что происходит с пользовательским объектом? Почему он возвращается? Как им можно управлять? Нужно ли в какой-то момент вызывать функцию выходаlogout? Разве это не должен быть простой геттер?

User u = UserService.getUser();

Обработка ошибок

Может, login()вернул User, чтобы потом вернуть nullпри ошибке? И велик соблазн вернуть код ошибок из команды, если она не может изменить состояние. Но лучше выбросить исключение и руководствоваться правилом о том, что команды возвращают void.

Подвохи

Несмотря на то, что разделение команд и запросов отлично работает в большинстве случаев, не обходится и без исключений. Удаление элемента из стека — это пример функции, которая изменяет состояние и возвращает результат в виде запроса.

Element e = Stack<Element>.pop(); // stateful query

Итог

  • Команды возвращают void, a запросы возвращают значения.
  • Пользуйтесь исключениями вместо возвращения и проверки на ошибки.

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

Перевод статьи Arun SasidharanObject Oriented Tricks: #1 The Art of Command Query Separation