Зарегистрируйтесь, чтобы продолжить обучение

Разделение команд и запросов Python: Функции

Command-query Separation (CQS) — принцип программирования, изобретённый Бертандом Майером, создателем языка Eiffel.

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

Команда


# Возвращает True или False как результат своего выполнения
save(user)

Согласно принципу CQS, функция save() является командой. Единственное, что она может - возвращать (опять же согласно принципу) успешность своего выполнения, то есть True или False, либо None, как, например, в случае с print(). Возврат этой функцией любых осмысленных данных, рассматривается как нарушение CQS. Однако, стоит сказать, что существуют ситуации, в которых невозможно соблюсти этот принцип. Например, открытие файла на запись возвращает файловый дескриптор, идентификатор, через который происходят манипуляции с файлом.

file = open('/etc/hosts', 'r')

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

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

Запрос


# Возвращает True или False
is_admin(user)

Функция is_admin() - предикат, типичный запрос (query) или, можно даже сказать, вопрос, который звучит так "Пользователь администратор?". Такая функция, с точки зрения CQS, не может изменять состояние системы, например, поменять дату проверки на администратора внутри пользователя или даже сделать пользователя администратором. Это противоречит не только CQS, но и здравому смыслу. В отличие от предыдущего примера, True и False в случае предикатов, это не успешность выполнения функции, а ответ на заданный вопрос.

Взгляните на пример работы функции, которая меняет исходные данные:


users = {
    {'name': 'Stan', 'kids': ['John', 'Mary']},
    {'name': 'Donald', 'kids': ['James']},
    {'name': 'Lily', 'kids': []},
    {'name': 'Julian', 'kids': []}
}

# Сперва кажется, что take_kids() возвращает список детей всех пользователей
take_kids(users) # ['John', 'Mary', 'James']
# Но на самом деле внутри она меняет словарь users и возвращает его наружу
print(users) # => ['John', 'Mary', 'James']

Если сделать еще один вызов take_kids(users), то выполнение кода, скорее всего, завершится с ошибкой, так как изменилась структура исходных данных. Такое поведение функции-запроса противоестественно. CQS имеет альтернативную формулировку, которая отлично характеризует код выше: "Задавая вопрос, не изменяй ответ".

К запросам относятся и любые вычисления:

max_number = max([1, 30, 4])

Этот код не создаёт никаких побочных эффектов и детерминирован. Его можно вызывать сколько угодно раз без риска получить ошибку или неверный результат.

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

Сложные ситуации

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

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

Логика этой функции выглядит так:

# Проверяем есть ли
username = get_name()

if not username:
    # Если имени нет, то генерируем и запоминаем
    username = generate_username()
    set_name(username)

Если убрать эту логику внутрь функции, то как она должна называться? Посмотрите на такой вариант:

username = get_username()

Такое имя функции вводит в заблуждение. Оно выглядит как запрос, get означает "получить", но не является им. Не зная устройство этой функции, невозможно догадаться о происходящем внутри. Первая мысль всегда будет о том, что это просто чтение каких-то уже записанных данных.

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

username = set_username()

# Или даже так
username = set_username_if_empty()

Дополнительные материалы

  1. Command-query Separation
  2. Принцип наименьшего удивления

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
Программирование на Python, Разработка веб-приложений и сервисов используя Django, проектирование и реализация REST API
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»