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

StackOverflow https://stackoverflow.com/questions/3688068

  •  02-10-2019
  •  | 
  •  

Вопрос

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

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

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

Например:

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

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

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

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

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

Я что-то упускаю?

Спасибо!

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

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

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

Это было полезно?

Решение

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

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

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

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

Другие советы

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

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

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

Чтобы ответить на вопрос, спросил: можно подумать о способе работы по выполнению команды и возврата результата. Например, предоставляя командный объект (командной шаблон), который имеет метод Void Execute и свойство Realonly Command Result.

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

Кстати.

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

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

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

Теперь вы можете иметь «командную» метод, как так:

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

Проблема с серой областью заключается в том, что она легко злоупотребляет. Установка возврата информации, таких как новая бронирование в монаде, позволит потребителям сказать: «Забудьте о событии, у нас есть идентификатор прямо здесь !!!» Также не все команды потребуют монады. Вы действительно должны проверить структуру вашего приложения, чтобы убедиться, что вы действительно достигли краевого корпуса.

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

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

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

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

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.

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

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

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

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

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

«Это позволяет вам использовать запросы по желанию без каких-либо беспокойства об изменении состояния системы».

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

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

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

мои книги:

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

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

метод.

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

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

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

Подобно ниже:

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

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top