How would one apply command query separation (CQS), when result data is needed from a command?

后端 未结 10 1999
慢半拍i
慢半拍i 2021-01-30 06:25

In wikipedia\'s definition of command query separation, it is stated that

More formally, methods should return a value only if they are referentially t

10条回答
  •  死守一世寂寞
    2021-01-30 07:05

    The question being; How do you apply CQS when you need the result of a command?

    The answer is: You don't. If you want to run a command and get back a result, you aren't using CQS.

    However, black and white dogmatic purity could be the death of the universe. There are always edge cases and grey areas. The problem is that you begin to create patterns that are a form of CQS, but no longer pure CQS.

    A Monad is a possibility. Instead of your Command returning void, you could return Monad. a "void" Monad might look like this:

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

    Now you can have a "Command" method like so:

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

    The problem with grey area is that it is easily abused. Putting return information such as the new OrderID in the Monad would allow consumers to say, "Forget waiting for the Event, we've got the ID right here!!!" Also, not all Commands would require a Monad. You really should check the structure of your application to ensure you have truly reached an edge case.

    With a Monad, now your command consumption might look like this:

    //some function child in the Call Stack of "CallBackendToCreateOrder"...
        var order = CreateNewOrder(buyer, item, transactionGuid);
        if (!order.Success || order.IsExceptionState)
            ... Do Something?
    

    In a codebase far far away . . .

    _eventHandler.on("orderCreated", transactionGuid, out order)
    _storeService.PerformPurchase(order);
    

    In a GUI far far away . . .

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

    Now you have all of the functionality and properness you want with just a bit of grey area for the Monad, but BEING SURE that you aren't accidentally exposing a bad pattern through the Monad, so you limit what you can do with it.

提交回复
热议问题