Какие операции и обязанности относятся к разделу «commands» в паттерне CQS?

Неанемичные сущности сами обновляют состояние через изменение атрибутов. Соответственно, метод update в командном сервисе зачастую кажется лишним. Что тогда стоит размещать в командном сервисе помимо save? Должны ли туда относиться SELECT‑запросы, используемые с целью изменить сущность? И если один и тот же запрос применяется и для изменения, и для отображения данных — где его лучше разместить?
  • 30 марта 2026 г. 20:02
Ответы на вопрос 2
Коротко о самом принципе. CQS (Command–Query Separation) говорит: метод либо команда — делает побочный эффект и не возвращает (или не обещает) значимое вычисляемое значение, либо запрос — возвращает данные и не имеет побочных эффектов. На практике это правило помогает разделять ответственность: кто меняет состояние, а кто читает его.

Что относится к «commands» (командной стороне)
- Операции, которые изменяют состояние системы: создание, удаление, любые изменения сущностей и агрегатов, транзакции, переключение состояний (approve, reject, publish, cancel и т. п.).
- Оркестрация сценариев применения бизнес‑логики: загрузка агрегата, вызов доменных методов на агрегате, проверка invariants, сохранение через репозиторий.
- Кросс‑срезовые обязанности, связанные с изменением состояния: валидация (application/command validation), авторизация действий, открытие/закрытие транзакций, публикация доменных/интеграционных событий, логирование, реализация идемпотентности, retry, компенсации и т. п.
- Вызовы внешних сервисов, которые являются частью выполнения команды (например уведомление после успешной модификации).

Почему «update» в командном сервисе кажется лишним
- Часто действительно лучше избегать обобщённого метода update(entityDto). Вместо этого предпочтительнее явно моделировать намерение: ChangeCustomerAddress, ApproveOrder, AddItemToCart. Так легче инкапсулировать бизнес‑правила и понять ответственность.
- Само изменение состояния должно происходить в доменной сущности/агрегате (неанемичная модель): command handler/сервис загружает агрегат, вызывает агрегатный метод (например order.addItem(...)), затем сохраняет агрегат через репозиторий.

Что размещать в командном сервисе помимо save
- Конкретные команды/handlers с описанными выше обязанностями (валидировать команду, проверять авторизацию, загружать агрегат, вызывать доменные методы, сохранять, публиковать события).
- Утилиты, связанные с выполнением команд (идемпотентность, трекинг, транзакции).
- Репозитории для загрузки/сохранения агрегатов (или вызовы на них). Но не презентационные запросы/проекции.

SELECT‑запросы: когда они относятся к commands, а когда к queries
- SELECT, который делается только внутри команды для того, чтобы загрузить агрегат/проверить условие перед изменением — это часть реализации команды. Он не является "запросом" в смысле CQS, потому что используется для подготовки побочного эффекта. Такие загрузки должны идти через репозиторий/агрегат‑фабрику.
- SELECT, который возвращает данные для отображения/отчёта/интерфейса — это query и принадлежит read‑сервису/проекциям/DAO для чтения.
- Внутри команды допустимы селекты для валидаций (например «есть ли у пользователя доступ», «существует ли товар»). Но это не превращает команду в query.

Если один и тот же запрос/SQL используется и для изменения, и для отображения
- Логика разделения по назначению, а не по SQL: для модификации используйте репозиторий/агрегат (rich domain object) — вы загружаете состояние в доменную модель, применяете поведение и сохраняете; для отображения — используйте отдельные read‑проекции/DTO, оптимизированные под чтение.
- Если технически вы дублируете SQL/проекцию, можно вынести низкоуровневый доступ (например DAO или общие SQL‑фрагменты), но сохранить семантическое разделение: вызовы для чтения — в query‑слое, для загрузки агрегата — в repository command‑слое.
- Не объединяйте публичные API: не возвращайте из команды UI DTO; команды возвращают только статус/идемпотентный результат/идентификатор, а запросы — данные для отображения.

Практические рекомендации
- Модель команд как «интент»: именуйте методы конкретно (ApproveInvoice, ChangePassword), а не generic update.
- Держите domain logic внутри сущностей/агрегатов; command handlers — оркестраторы.
- Репозитории отвечают за загрузку/сохранение агрегатов; query‑репозитории/проекции — за быстрые SELECT для UI.
- SELECTы, используемые только для проверки/загрузки в рамках изменения, являются частью командной реализации и не должны становиться общедоступными query‑методами.
- Если экономия кода подталкивает к совместному использованию SQL — делайте это через общий низкоуровневый слой, но держите семантическое разделение слоёв.

Итог
Commands — это всё, что меняет состояние + связанные оркестрационные обязанности (транзакция, валидация, авторизация, публикация событий). SELECT‑запросы, служащие только для загрузки агрегата перед изменением, — часть реализации команды (через репозиторий). SELECT для отображения — в query/ReadModel. Если один и тот же SQL нужен и там, и там — можно вынести общий DAO, но не смешивайте слои на уровне API и семантики.
у нас на проекте commands были юзкейсы: <code>cancelOrder(id)</code> , <code>changePassword(userId, newPass)</code> . Внутри загружаешь entity через репо, дёргаешь доменный метод, сохраняешь. Вся эта цепочка = одна команда, а не голый save. <br/> <br/> А то что один <code>findById()</code> юзается и в query-сервисе и внутри команды — норм, CQS делит публичный интерфейс, не внутренние запросы.
Похожие вопросы