Как можно применить разделение командного запроса (CQS), когда от команды требуются данные результата? - PullRequest
58 голосов
/ 11 сентября 2010

В определении команды Википедии указано, что

Более формально, методы должны возвращать только значение если они прозрачны по ссылкам и, следовательно, не имеют побочных эффектов.

Если я выполняю команду, как мне определить или сообщить, была ли эта команда успешной, поскольку по этому определению функция не может возвращать данные?

Например:

string result = _storeService.PurchaseItem(buyer, item);

Этот вызов содержит как команду, так и запрос, но часть запроса является результатом команды. Я думаю, что я мог бы изменить его с помощью шаблона команды, например так:

PurchaseOrder order = CreateNewOrder(buyer, item);
_storeService.PerformPurchase(order);
string result = order.Result;

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

Может кто-нибудь дать мне лучший способ добиться разделения команд и запросов, когда вам нужен результат операции?

Я что-то здесь упускаю?

Спасибо!

Примечания: Мартин Фаулер говорит об ограничениях cqs CommandQuerySeparation :

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

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

Ответы [ 9 ]

40 голосов
/ 15 июля 2011

Этот вопрос старый, но еще не получил удовлетворительного ответа, поэтому я немного уточню свой комментарий почти год назад.

Использование управляемой событиями архитектуры имеет большой смысл не только для достижения четкого разделения команд / запросов, но также и потому, что оно открывает новые архитектурные решения и обычно соответствует модели асинхронного программирования (полезно, если вам нужно масштабировать свою архитектуру) , Чаще всего решение может заключаться в том, чтобы по-другому моделировать ваш домен.

Итак, давайте возьмем ваш пример покупки. StoreService.ProcessPurchase будет подходящей командой для обработки покупки. Это сгенерирует PurchaseReceipt. Это лучший способ, чем возвращать квитанцию ​​в Order.Result. Для простоты вы можете вернуть квитанцию ​​от команды и нарушить CQRS здесь. Если вы хотите более четкое разделение, команда вызовет событие ReceiptGenerated, на которое вы можете подписаться.

Если вы думаете о своем домене, это может быть лучшей моделью. Когда вы проверяете в кассе, вы следуете этому процессу. До получения квитанции может потребоваться проверка кредитной картой. Это может занять больше времени. В синхронном сценарии вы будете ждать в кассе, не в состоянии сделать что-либо еще.

12 голосов
/ 09 сентября 2015

Я вижу много путаницы между CQS и CQRS (как заметил Марк Роджерс и в одном ответе).

CQRS - это архитектурный подход в DDD, при котором в случае запроса вы не строите полноразмерные графы объектов из совокупных корней со всеми их сущностями и типами значений, а просто облегчаете представления объектов в виде списка.

CQS - это хороший принцип программирования на уровне кода в любой части вашего приложения. Не только доменная зона. Принцип существует намного дольше, чем DDD (и CQRS). Он говорит не путать команды, которые изменяют любое состояние приложения, с запросами, которые просто возвращают данные и могут быть вызваны в любое время без изменения какого-либо состояния. В мои старые дни с Delphi язык показал разницу между функциями и процедурами. Считалось плохой практикой кодировать «функциональные процедуры», как мы их отозвали.

Чтобы ответить на заданный вопрос: Можно придумать способ обойти выполнение команды и получить результат. Например, предоставляя объект команды (шаблон команды), который имеет метод void execute и свойство результата команды readonly.

Но какова основная причина придерживаться CQS? Сохраняйте код читабельным и многоразовым без необходимости смотреть на детали реализации. Ваш код должен быть надежным, чтобы не вызывать неожиданные побочные эффекты. Поэтому, если команда хочет вернуть результат, а имя функции или возвращаемый объект четко указывает, что это команда с результатом команды, я приму исключение из правила CQS. Не нужно делать вещи более сложными. Я согласен с Мартином Фаулером (упомянутым выше) здесь.

Кстати: не будет ли строго следовать этому правилу нарушить весь принцип свободного владения API?

2 голосов
/ 15 августа 2018

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

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 => {...});

Надеюсь, это поможет любой потерянной душе ...

2 голосов
/ 16 мая 2017

Вопрос в том; Как вы применяете 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);

Теперь у вас есть все необходимые функциональные возможности и правильность с небольшим количеством серой области для Монады, но УБЕДИТЕСЬ, что вы случайно не выставляете плохой шаблон через Монаду, поэтому вы ограничиваете то, что можете делать с это.

2 голосов
/ 22 сентября 2010

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

Я не говорю, что это универсальное решение, но переключение на более сильную модель «исключение при сбое» вместо модели «отправить ответ» мне очень помогло, чтобы разделение действительно работало в моем собственном коде. Конечно, в конечном итоге вам придется написать гораздо больше обработчиков исключений, так что это компромисс ... Но это как минимум другой аспект для рассмотрения.

1 голос
/ 15 августа 2018

Потратьте еще немного времени на то, чтобы подумать, ПОЧЕМУ вы хотите разделить командные запросы.

"Это позволяет вам использовать запросы по своему усмотрению, не беспокоясь об изменении состояния системы."

Так что можно возвращать значение из команды, чтобы позволить вызывающей сторонезнаю, что ему удалось

, потому что было бы расточительно создавать отдельный запрос с единственной целью

, чтобы выяснить, правильно ли работала предыдущая команда.Нечто подобное хорошо в

моих книгах:

boolean succeeded = _storeService.PurchaseItem(buyer, item);

Недостаток вашего примера в том, что неясно, что возвращает ваш

метод.

string result = _storeService.PurchaseItem(buyer, item);

Непонятно, что такое «результат».

Использование CQS (разделение командного запроса) позволяет вам сделать вещи более очевидными

, как показано ниже:

if(_storeService.PurchaseItem(buyer, item)){

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

Да, это больше кода, но более понятно, что происходит.

1 голос
/ 18 июля 2017

Я действительно опаздываю на это, но есть еще несколько вариантов, которые не были упомянуты (хотя, не уверен, действительно ли они так хороши):

Одна из опций, которую я раньше не видел, - это создание другого интерфейса для реализации обработчика команд. Возможно ICommandResult<TCommand, TResult>, который реализует обработчик команд. Затем, когда запускается обычная команда, она устанавливает результат для результата команды, и вызывающая сторона затем извлекает результат через интерфейс ICommandResult. С IoC вы можете сделать так, чтобы он возвращал тот же экземпляр, что и обработчик команд, чтобы вы могли получить результат обратно. Хотя это может нарушить SRP.

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

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

Редактировать

Поработав над этим, я создал «CommandQuery». Это гибрид между командой и запросом, очевидно. :) Если есть случаи, когда вам нужен этот функционал, то вы можете его использовать. Однако для этого должна быть действительно веская причина. Он НЕ будет повторяться и не может быть кэширован, поэтому есть различия по сравнению с двумя другими.

0 голосов
/ 13 сентября 2010

CQS в основном используется при реализации доменно-управляемого проектирования, и поэтому вам следует (как утверждает и Одед) использовать управляемую событиями архитектуру для обработки результатов.Поэтому ваш string result = order.Result; всегда будет в обработчике событий, а не непосредственно после него в коде.

Ознакомьтесь с этой замечательной статьей , в которой показана комбинация CQS, DDD и EDA.

...