How would one apply command query separation (CQS)

2019-03-08 01:31发布

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

More formally, methods should return a value only if they are referentially transparent and hence possess no side effects.

If I am issuing a command, how should I determine or report whether that command was successful, since by this definition the function cannot return data?

For example:

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

This call has both a command and query in it, but the query portion is result of the command. I guess I could refactor this using the command pattern, like so:

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

But this seems like it's increasing the size and complexity of the code, which is not a very positive direction to refactor towards.

Can someone give me a better way to achieve command-query separation when you need the result of an operation?

Am I missing something here?

Thanks!

Notes: Martin Fowler has this to say about the limits of cqs CommandQuerySeparation:

Meyer likes to use command-query separation absolutely, but there are exceptions. Popping a stack is a good example of a modifier that modifies state. Meyer correctly says that you can avoid having this method, but it is a useful idiom. So I prefer to follow this principle when I can, but I'm prepared to break it to get my pop.

From his view, it's almost always worth it to refactor towards command/query separation, except for a few minor simple exceptions.

9条回答
男人必须洒脱
3楼-- · 2019-03-08 01:51

Take some more time to think about WHY you want Command Query Separation.

"It lets you use queries at will without any worry of changing system state."

So it is OKAY to return a value from a command to let the caller know it succeeded

because it would be wasteful to create a separate query for the sole purpose of

finding out if a previous command worked properly. Something like this is okay in

my books:

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

A disadvantage of your example is that it is not obvious what is returned by your

method.

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

It is not clear what 'result' is exactly.

Using CQS (Command Query Seperation) allows you to make things more obvious

similar to below:

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

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

Yes, this is more code, but it is more clear what is happening.

查看更多
我欲成王,谁敢阻挡
4楼-- · 2019-03-08 02:01

I see a lot of confusion above between CQS & CQRS (as Mark Rogers noticed at one answer as well).

CQRS is an architectural approach in DDD where, in case of a query, you do not build up full blown object graphs from aggregate roots with all their entities and value types, but just lightweight view objects to show in a list.

CQS is a good programming principle on code level in any part of your application. Not just the domain area. The principle exists way longer than DDD (and CQRS). It says not to mess up commands that change any state of the application with queries that just return data and can be invoked any time without changing any state. In my old days with Delphi, the lanquage showed a difference between functions and procedures. It was considered a bad practice to code 'functionprocedures' as we called them back than as well.

To answer the question asked: One could think of a way to work around executing a command and getting back a result. For instance by providing a command object (command pattern) which has a void execute method and a readonly command result property.

But what is the main reason to adhere to CQS? Keep code readable and reusable without the need to look at implementation details. Your code should be trustworthy not to cause unexpected side effects. So if the command wants to return a result, and the funcion name or return object clearly indicates that it is a command with a command result, I'll accept the exception to the CQS rule. No need to make things more complex. I agree with Martin Fowler (mentioned above) here.

By the way: wouldn't strictly following this rule break the whole fluent api principle?

查看更多
等我变得足够好
5楼-- · 2019-03-08 02:03

I like the event driven architecture suggestions other people have given, but I just want to throw in another point of view. Maybe you need to look at why you're actually returning data from your command. Do you actually need the result out of it, or could you get away with throwing an exception if it fails?

I'm not saying this as a universal solution, but switching to a stronger "exception on failure" instead of "send back a response" model helped me a lot in making the separation actually work in my own code. Of course, then you end up having to write a lot more exception handlers, so it's a trade off... But it's at least another angle to consider.

查看更多
聊天终结者
6楼-- · 2019-03-08 02:04

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.

查看更多
The star\"
7楼-- · 2019-03-08 02:06

Well, this is a pretty old question but I post this just for the record. Whenever you use an event, you can instead use a delegate. Use events if you have many interested parties, otherwise use a delegate in a callback style:

void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated)

you can also have a block for the case where the operation failed

void CreateNewOrder(Customer buyer, Product item, Action<Order> onOrderCreated, Action<string> onOrderCreationFailed)

This decrease the cyclomatic complexity on the client code

CreateNewOrder(buyer: new Person(), item: new Product(), 
              onOrderCreated: order=> {...},
              onOrderCreationFailed: error => {...});

Hope this helps any lost soul out there...

查看更多
登录 后发表回答