Как можно применить разделение командного запроса (CQS), когда от команды требуются данные результата?
В определении разделения командного запроса в Википедии говорится, что
Более формально, методы должны возвращать значение, только если они прозрачны по ссылкам и, следовательно, не имеют побочных эффектов.
Если я выполняю команду, как мне определить или сообщить, была ли эта команда успешной, поскольку по этому определению функция не может возвращать данные?
Например:
string result = _storeService.PurchaseItem(buyer, item);
Этот вызов содержит команду и запрос, но часть запроса является результатом команды. Я думаю, что я мог бы реорганизовать это с помощью шаблона команды, например, так:
PurchaseOrder order = CreateNewOrder(buyer, item);
_storeService.PerformPurchase(order);
string result = order.Result;
Но похоже, что это увеличивает размер и сложность кода, что не очень позитивное направление для рефакторинга.
Может кто-нибудь дать мне лучший способ добиться разделения команд и запросов, когда вам нужен результат операции?
Я что-то здесь упускаю?
Спасибо!
Примечания: Мартин Фаулер может сказать об ограничениях cqs CommandQuerySeparation:
Мейер любит использовать разделение команд и запросов абсолютно, но есть исключения. Popping стек является хорошим примером модификатора, который изменяет состояние. Мейер правильно говорит, что вы можете избежать использования этого метода, но это полезная идиома. Поэтому я предпочитаю следовать этому принципу, когда смогу, но я готов нарушить его, чтобы получить популярность.
По его мнению, почти всегда стоит проводить рефакторинг в сторону разделения команд и запросов, за исключением нескольких незначительных простых исключений.
9 ответов
Этот вопрос старый, но еще не получил удовлетворительного ответа, поэтому я немного уточню свой комментарий почти год назад.
Использование управляемой событиями архитектуры имеет большой смысл не только для достижения четкого разделения команд / запросов, но также и потому, что оно открывает новые архитектурные решения и обычно соответствует модели асинхронного программирования (полезно, если вам нужно масштабировать свою архитектуру). Чаще всего вы обнаружите, что решение может заключаться в том, чтобы по-другому моделировать ваш домен.
Итак, давайте возьмем ваш пример покупки. StoreService.ProcessPurchase
будет подходящей командой для обработки покупки. Это будет генерировать PurchaseReceipt
, Это лучший способ, чем возвращать квитанцию в Order.Result
, Для простоты вы можете вернуть квитанцию от команды и нарушить CQRS здесь. Если вы хотите более четкое разделение, команда выдаст ReceiptGenerated
событие, на которое вы можете подписаться.
Если вы думаете о своем домене, это может быть лучшей моделью. Когда вы проверяете в кассе, вы следуете этому процессу. До получения квитанции может потребоваться проверка кредитной карты. Это может занять больше времени. В синхронном сценарии вы будете ждать в кассе, не в состоянии сделать что-либо еще.
Я вижу много путаницы между CQS и CQRS (как заметил Марк Роджерс и в одном ответе).
CQRS - это архитектурный подход в DDD, при котором в случае запроса вы не строите полноразмерные графы объектов из совокупных корней со всеми их сущностями и типами значений, а просто облегчаете представления объектов в виде списка.
CQS - это хороший принцип программирования на уровне кода в любой части вашего приложения. Не только доменная зона. Принцип существует намного дольше, чем DDD (и CQRS). В нем говорится не путать команды, которые изменяют любое состояние приложения, с запросами, которые просто возвращают данные и могут быть вызваны в любое время без изменения какого-либо состояния. В мои старые дни с Delphi язык показал разницу между функциями и процедурами. Считалось плохой практикой кодировать "функциональные процедуры", как мы их отозвали.
Чтобы ответить на заданный вопрос: можно придумать способ обойти выполнение команды и получить результат. Например, предоставляя объект команды (шаблон команды), который имеет метод void execute и свойство результата команды readonly.
Но какова основная причина придерживаться CQS? Сохраняйте код читабельным и многократно используемым без необходимости смотреть на детали реализации. Ваш код должен быть надежным, чтобы не вызывать неожиданные побочные эффекты. Поэтому, если команда хочет вернуть результат, а имя функции или возвращаемый объект четко указывают, что это команда с результатом команды, я приму исключение из правила CQS. Не нужно делать вещи более сложными. Я согласен с Мартином Фаулером (упомянутым выше) здесь.
Кстати, не будет ли строго следовать этому правилу весь принцип свободного владения API?
Эти ссылки могут помочь
О, это интересно. Наверное, мне тоже есть что сказать.
В последнее время я использую неортодоксальный подход CQS (может быть, вообще не CQS для кого-то, но мне все равно), который помогает избежать беспорядочного репозитория (потому что кто использует шаблон спецификации, да?), Реализации и службы Layer-классы, которые со временем значительно разрастаются, особенно в огромных проектах. Проблема в том, что это происходит, даже если все остальное в порядке и разработчики довольно опытны, потому что (сюрприз), если у вас большой класс, это не всегда означает, что он в первую очередь нарушает SRP. И общий подход, который я вижу в таких проектах, очень часто: "О, у нас есть огромные классы, давайте разделим их", и это разделение в основном синтетическое, а не естественное развитие. Итак, что делают люди, чтобы с этим справиться? Из одного делают несколько классов.Но что происходит с DI в огромном проекте, когда у вас внезапно появляется в несколько раз больше классов, чем раньше? Не очень хорошая картина, поскольку DI, вероятно, уже изрядно загружен инъекциями. Таким образом, существуют обходные пути, такие как шаблон фасада и т. Д. (Если применимо), и в результате мы: не предотвращаем проблему; иметь дело только с последствиями и тратить на это много времени; часто используют "синтетический" подход к рефакторингу; получить меньше зла вместо большего зла, но все равно это зло.синтетический "подход к рефакторингу: меньше зла вместо большего зла, но все же это зло.синтетический "подход к рефакторингу: меньше зла вместо большего зла, но все же это зло.
Что мы делаем вместо этого? Мы применяем KISS и YAGNI к CQS в качестве первого шага.
- Используйте Commands/CommandHandlers и Queries/QueryHandlers.
- Используйте универсальный объект возврата как для запросов, так и для команд, которые содержат результат и ошибку (ай!).
- Избегайте стандартных реализаций служб и репозиториев по умолчанию - только если это строго необходимо.
Какие проблемы решает такой подход?
- Раннее предотвращение беспорядка в коде, намного проще работать и масштабироваться (будущее).
- Вы не поверите, но для среднего проекта у нас вообще не было ни классов обслуживания, ни репозиториев. Чем больше проект, тем выгоднее такой подход (если предположить, что CQRS и ES не нужны и сравнить только со стандартными уровнями сервис + данные). И мы очень довольны этим, так как этого более чем достаточно для большинства средних проектов с точки зрения затрат и эффективности.
Так что бы я посоветовал тебе сделать?
- Используйте правильный инструмент для правильной работы. Используйте подход, который решает ваши проблемы, и избегайте делать все по инструкции, если это сопряжено с ненужной сложностью для вашего дела "просто потому, что поэтому". Кстати, как часто вы видите полностью RESTful Level 3 API?..
- Не используйте ничего, если вам это не нужно, и особенно если вы этого не понимаете, потому что, если вы этого не сделаете, это принесет больше вреда, чем пользы. CQRS хорош в некоторых случаях и по-прежнему довольно прост для понимания, но требует разработки и поддержки; ES довольно сложно понять, а еще труднее создавать и поддерживать.
Вопрос быть; Как вы применяете CQS, когда вам нужен результат команды?
Ответ: вы не делаете. Если вы хотите выполнить команду и получить результат, вы не используете CQS.
Однако черно-белая догматическая чистота могла стать смертью вселенной. Всегда есть крайние случаи и серые области. Проблема в том, что вы начинаете создавать шаблоны, которые являются формой CQS, но больше не являются чистыми CQS.
Монада это возможность. Вместо вашей Команды, возвращающей пустоту, вы можете вернуть Монаду. "пустая" монада может выглядеть так:
public class Monad {
private Monad() { Success = true; }
private Monad(Exception ex) {
IsExceptionState = true;
Exception = ex;
}
public static Monad Success() => new Monad();
public static Monad Failure(Exception ex) => new Monad(ex);
public bool Success { get; private set; }
public bool IsExceptionState { get; private set; }
public Exception Exception { get; private set; }
}
Теперь у вас может быть метод "Command", например:
public Monad CreateNewOrder(CustomerEntity buyer, ProductEntity item, Guid transactionGuid) {
if (buyer == null || string.IsNullOrWhiteSpace(buyer.FirstName))
return Monad.Failure(new ValidationException("First Name Required"));
try {
var orderWithNewID = ... Do Heavy Lifting Here ...;
_eventHandler.Raise("orderCreated", orderWithNewID, transactionGuid);
}
catch (Exception ex) {
_eventHandler.RaiseException("orderFailure", ex, transactionGuid); // <-- should never fail BTW
return Monad.Failure(ex);
}
return Monad.Success();
}
Проблема с серой областью заключается в том, что ею легко злоупотреблять. Размещение информации о возврате, такой как новый OrderID, в Monad позволит потребителям сказать: "Забудьте, ожидая Событие, у нас есть идентификатор прямо здесь!!!" Кроме того, не все команды требуют монады. Вы действительно должны проверить структуру своего приложения, чтобы убедиться, что вы действительно достигли крайнего случая.
С Monad, теперь ваше командное потребление может выглядеть так:
//some function child in the Call Stack of "CallBackendToCreateOrder"...
var order = CreateNewOrder(buyer, item, transactionGuid);
if (!order.Success || order.IsExceptionState)
... Do Something?
В кодовой базе далеко-далеко.,,
_eventHandler.on("orderCreated", transactionGuid, out order)
_storeService.PerformPurchase(order);
В графическом интерфейсе далеко-далеко.,,
var transactionID = Guid.NewGuid();
OnCompletedPurchase(transactionID, x => {...});
OnException(transactionID, x => {...});
CallBackendToCreateOrder(orderDetails, transactionID);
Теперь у вас есть все функциональные возможности и правильность, которые вы хотите, с небольшим количеством серой области для Монады, но УБЕДИТЕСЬ, что вы случайно не выставляете плохой шаблон через Монаду, поэтому вы ограничиваете то, что можете с ним делать.
Потратьте еще немного времени, чтобы подумать, ПОЧЕМУ вы хотите разделить командные запросы.
"Это позволяет использовать запросы по желанию, не беспокоясь об изменении состояния системы".
Так что все в порядке, чтобы вернуть значение из команды, чтобы звонящий знал, что это успешно
потому что было бы расточительно создать отдельный запрос с единственной целью
выяснить, работала ли предыдущая команда правильно. Как-то так в порядке
мои книги:
boolean succeeded = _storeService.PurchaseItem(buyer, item);
Недостатком вашего примера является то, что не очевидно, что возвращается вашим
метод.
string result = _storeService.PurchaseItem(buyer, item);
Непонятно, что такое "результат".
Использование CQS (Command Query Seperation) позволяет сделать вещи более очевидными
похож на ниже:
if(_storeService.PurchaseItem(buyer, item)){
String receipt = _storeService.getLastPurchaseReciept(buyer);
}
Да, это больше кода, но это более понятно, что происходит.
Мне нравятся предложения по архитектуре, управляемой событиями, которые давали другие люди, но я просто хочу добавить другую точку зрения. Может быть, вам нужно посмотреть, почему вы на самом деле возвращаете данные из вашей команды. Вам действительно нужен результат из этого, или вы могли бы сойти с рук, выдав исключение в случае неудачи?
Я не говорю, что это универсальное решение, но переключение на более сильную модель "исключение при сбое" вместо модели "отправить ответ" мне очень помогло в том, чтобы разделение действительно работало в моем собственном коде. Конечно, в конечном итоге вам придется написать гораздо больше обработчиков исключений, так что это компромисс... Но это как минимум еще один аспект для рассмотрения.
Ну, это довольно старый вопрос, но я публикую его только для записи. Всякий раз, когда вы используете событие, вы можете вместо этого использовать делегата. Используйте события, если у вас много заинтересованных сторон, в противном случае используйте делегата в стиле обратного вызова:
void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated)
Вы также можете иметь блок для случая, когда операция не удалась
void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated, Action<string> onOrderCreationFailed)
Это уменьшает цикломатическую сложность клиентского кода
CreateNewOrder(buyer: new Person(), item: new Product(),
onOrderCreated: order=> {...},
onOrderCreationFailed: error => {...});
Надеюсь, это поможет любой потерянной душе там...
Я очень опоздал на это, но есть еще несколько вариантов, которые не были упомянуты (хотя, не уверен, действительно ли они так хороши):
Одна из опций, которую я раньше не видел, - это создание другого интерфейса для реализации обработчика команд. Может быть ICommandResult<TCommand, TResult>
что реализует обработчик команд. Затем, когда запускается обычная команда, она устанавливает результат для результата команды, и вызывающая сторона извлекает результат через интерфейс ICommandResult. С IoC вы можете сделать так, чтобы он возвращал тот же экземпляр, что и обработчик команд, чтобы вы могли получить результат обратно. Хотя это может нарушить SRP.
Другим вариантом является наличие своего рода общего хранилища, которое позволяет отображать результаты команд таким образом, чтобы запрос мог затем получить их. Например, скажем, в вашей команде было много информации, а затем был идентификатор OperationId или что-то в этом роде. Когда команда завершает работу и получает результат, она отправляет ответ либо в базу данных с этим идентификатором OperationId Guid в качестве ключа, либо в некоторый тип общего / статического словаря в другом классе. Когда вызывающий абонент получает контроль назад, он вызывает запрос на откат, основанный на результате, основанном на заданном Guid.
Самый простой ответ - просто вставить результат в саму Команду, но это может сбить некоторых с толку. Другой вариант, о котором я упоминал, - это события, которые вы можете сделать технически, но если вы находитесь в веб-среде, это значительно усложняет обработку.
редактировать
После работы с этим больше, я закончил тем, что создал "CommandQuery". Это гибрид между командой и запросом, очевидно.:) Если есть случаи, когда вам нужен этот функционал, то вы можете его использовать. Однако для этого должна быть действительно веская причина. Это НЕ будет повторяться и не может быть кэшировано, поэтому есть различия по сравнению с двумя другими.
CQS в основном используется при реализации доменно-управляемого проектирования, и поэтому вам следует (как утверждает и Одед) использовать управляемую событиями архитектуру для обработки результатов. Ваш string result = order.Result;
поэтому всегда будет в обработчике событий, а не непосредственно после этого в коде.
Проверьте эту замечательную статью, которая показывает комбинацию CQS, DDD и EDA.