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

冷暖自知 提交于 2019-12-03 01:26:30

问题


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.


回答1:


This question is old but has not received a satisfying answer yet, so I'll elaborate a bit on my comment from almost a year ago.

Using an event driven architecture makes a lot of sense, not only for achieving clear command/query separation, but also because it opens new architectural choices and usually fits with an asynchronous programming model (useful if you need to scale your architecture). More often than not, you will find the solution may lie in modelling your domain differently.

So let's take your purchase example. StoreService.ProcessPurchase would be a suitable command for processing a purchase. This would generate a PurchaseReceipt. This is a better way instead of returning the receipt in Order.Result. To keep things very simple, you can return the receipt from the command and violate CQRS here. If you want a cleaner separation, the command would raise a ReceiptGenerated event that you can subscribe to.

If you think about your domain, this may actually be a better model. When you're checking out at a cashier, you follow this process. Before your receipt is generated, a credit card check might be due. This is likely to take longer. In a synchronous scenario, you would wait at the cashier, unable to do anything else.




回答2:


These links may help

  • Meanwhile... on the command side of my architecture
  • Returning data from command handlers
  • Meanwhile... on the query side of my architecture
  • and also this...



回答3:


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 function 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?




回答4:


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 purchaseSucceeded = _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.




回答5:


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:


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.




回答7:


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...




回答8:


I'm really late to this, but there are a few more options that haven't been mentioned (though, not sure if they are really that great):

One option I haven't seen before is creating another interface for the command handler to implement. Maybe ICommandResult<TCommand, TResult> that the command handler implements. Then when the normal command runs, it sets the result on the command result and the caller then pulls out the result via the ICommandResult interface. With IoC, you can make it so it returns the same instance as the Command Handler so you can pull the result back out. Though, this might break SRP.

Another option is to have some sort of shared Store that lets you map results of commands in a way that a Query could then retrieve. For example, say your command had a bunch of information and then had an OperationId Guid or something like that. When the command finishes and gets the result, it pushes the answer either to the database with that OperationId Guid as the key or some sort of shared/static dictionary in another class. When the caller gets back control, it calls a Query to pull back based the result based on the given Guid.

The easiest answer is to just push the result on the Command itself, but that might be confusing to some people. The other option I see mentioned is events, which you can technically do, but if you are in a web environment, that makes it much more difficult to handle.

Edit

After working with this more, I ended up creating a "CommandQuery". It is a hybrid between command and query, obviously. :) If there are cases where you need this functionality, then you can use it. However, there needs to be really good reason to do so. It will NOT be repeatable and it cannot be cached, so there are differences compared to the other two.




回答9:


CQS is mainly used when implementing Domain Driven Design, and therefore you should (as Oded also states) use an Event Driven Architecture to process the results. Your string result = order.Result; would therefore always be in an event handler, and not directly afterwards in code.

Check out this great article which shows a combination of CQS, DDD and EDA.



来源:https://stackoverflow.com/questions/3688068/how-would-one-apply-command-query-separation-cqs-when-result-data-is-needed-f

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!