Подтвердить что ты не робот

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

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

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

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

Например:

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

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

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

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

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

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

Спасибо!

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 3

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

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

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

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

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

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

Ответ 4

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

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

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

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

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

мои книги:

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

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

Метод.

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

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

Использование CQS (Command Query Seperation) позволяет сделать вещи более очевидными

похож на ниже:

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

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

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

Ответ 5

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

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

Ответ 6

Вопрос: Как применить 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 теперь ваше потребление команд может выглядеть так:

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

В GUI далеко далеко.,.

var transactionID = Guid.NewGuid();
OnCompletedPurchase(transactionID, x => {...});
OnException(transactionID, x => {...});
CallBackendToCreateOrder(orderDetails, transactionID);

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

Ответ 7

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

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

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

Ответ 8

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

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

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

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

Edit

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

Ответ 9

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

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