Как можно применить разделение командных запросов (CQS), когда данные результата необходимы из команды?


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

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

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

для пример:

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

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

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

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

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

Я что-то пропустил?

спасибо!

заметки: Мартин Фаулер имеет это сказать о пределах cqs CommandQuerySeparation:

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

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

9 52

9 ответов:

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

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

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

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

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

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

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

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

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

кстати: не будет ли строго следовать этому правилу нарушать весь принцип fluent api?

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

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

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

теперь вы можете иметь "командный" метод, например:

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 в монаде, позволит потребителям сказать: "Забудьте ждать события, у нас есть идентификатор прямо здесь!!!"Кроме того, не все команды требуют монады. Вы действительно должны проверить структуру ваше приложение, чтобы убедиться, что вы действительно достигли крайнего случая.

С монадой, теперь ваше потребление команды может выглядеть так:

//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);

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

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

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

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

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

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

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

Edit

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

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

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

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

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

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

Так что это нормально, чтобы вернуть значение из команды, чтобы абонент знал, что это удалось

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

выяснение, если предыдущая команда работала правильно. Что-то вроде этого в порядке

мои книги:

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);
}

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