Это мини-серия статей по написанию поддерживаемого объектно-ориентированного кода без лишней нервотрепки.
У функций есть побочные эффекты. Иногда они изменяют состояние системы в самый неожиданный момент и рушат все, что только можно. Крайне трудно избавиться от побочных эффектов с помощью парадигмы объектно-ориентированного программирования. Поэтому нужно научиться управлять этими эффектами — тогда они не испортят систему при первой возможности.
Управление побочными эффектами
Отличным способом управления побочными эффектами является создание четкого разделения команд и запросов. В данном контексте команда изменяет систему и обладает побочным эффектом. Запрос возвращает вычисленное значение или наблюдаемое состояние системы.
Пример
Вы ждете, что при вызове функции 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 Sasidharan: Object Oriented Tricks: #1 The Art of Command Query Separation