69

在维基百科对命令查询分离的定义中,指出

更正式地说,方法只有在引用透明并且没有副作用时才应该返回一个值。

如果我发出命令,我应该如何确定或报告该命令是否成功,因为根据这个定义,函数不能返回数据?

例如:

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

此调用中既有命令也有查询,但查询部分是命令的结果。我想我可以使用命令模式重构它,如下所示:

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

但这似乎增加了代码的大小和复杂性,这不是一个非常积极的重构方向。

当您需要操作结果时,有人可以给我一个更好的方法来实现命令查询分离吗?

我在这里错过了什么吗?

谢谢!

注意:Martin Fowler 对 cqs CommandQuerySeparation的限制有这样的说法:

Meyer 绝对喜欢使用命令-查询分离,但也有例外。弹出堆栈是修改状态的修饰符的一个很好的例子。Meyer 正确地说您可以避免使用这种方法,但它是一个有用的成语。所以我更愿意尽可能地遵循这个原则,但我准备打破它以获得我的流行。

在他看来,几乎总是值得重构命令/查询分离,除了一些小的简单例外。

4

9 回答 9

45

这个问题很老,但还没有得到令人满意的答案,所以我将详细说明我大约一年前的评论。

使用事件驱动架构很有意义,不仅可以实现清晰的命令/查询分离,还因为它打开了新的架构选择,并且通常适合异步编程模型(如果您需要扩展架构,这很有用)。通常,您会发现解决方案可能在于对您的域进行不同的建模。

因此,让我们以您的购买为例。StoreService.ProcessPurchase将是处理购买的合适命令。这将生成一个PurchaseReceipt. 这是一种更好的方法,而不是退回收据Order.Result。为简单起见,您可以从命令返回收据并在此处违反 CQRS。如果您想要更清晰的分隔,该命令将引发ReceiptGenerated您可以订阅的事件。

如果您考虑您的域,这实际上可能是一个更好的模型。当您在收银台结账时,请遵循此流程。在生成收据之前,可能需要进行信用卡检查。这可能需要更长的时间。在同步场景中,您将在收银台等待,无法做任何其他事情。

于 2011-07-15T08:03:19.587 回答
20

我在上面看到 CQS 和 CQRS 之间存在很多混淆(正如 Mark Rogers 在一个答案中所注意到的那样)。

CQRS 是 DDD 中的一种架构方法,在查询的情况下,您不会从聚合根及其所有实体和值类型构建完整的对象图,而只是在列表中显示的轻量级视图对象。

CQS 是应用程序任何部分的代码级别的良好编程原则。不仅仅是领域领域。该原理比 DDD(和 CQRS)存在的时间更长。它说不要用只返回数据并且可以随时调用而不改变任何状态的查询来搞乱改变应用程序任何状态的命令。在我以前使用 Delphi 的时候,语言显示出功能和过程之间的差异。编码“函数过程”被认为是一种不好的做法,因为我们也将它们召回。

要回答所提出的问题:人们可以想出一种方法来解决执行命令并返回结果的问题。例如,通过提供一个命令对象(命令模式),它具有一个 void 执行方法和一个只读命令结果属性。

但是坚持CQS的主要原因是什么?保持代码的可读性和可重用性,无需查看实现细节。你的代码应该是值得信赖的,不会引起意想不到的副作用。所以如果命令要返回一个结果,而函数名或返回对象清楚地表明它是一个带有命令结果的命令,我会接受CQS规则的例外。没有必要让事情变得更复杂。我同意 Martin Fowler(上面提到的)在这里。

顺便说一句:严格遵守这条规则不会破坏整个流畅的api原则吗?

于 2015-09-09T08:43:20.820 回答
3

花更多时间思考为什么需要命令查询分离。

“它让您可以随意使用查询,而不必担心更改系统状态。”

所以可以从命令中返回一个值让调用者知道它成功了

因为仅出于以下目的创建单独的查询是浪费的

查明先前的命令是否正常工作。这样的事情在

我的书:

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

您的示例的一个缺点是您返回的内容并不明显

方法。

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

目前尚不清楚“结果”究竟是什么。

使用 CQS(命令查询分离)可以让事情变得更明显

类似于下面:

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

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

是的,这是更多的代码,但更清楚的是发生了什么。

于 2018-08-14T22:14:47.510 回答
3

问题是;当您需要命令的结果时,如何应用 CQS?

答案是:你没有。如果你想运行一个命令并返回一个结果,你没有使用 CQS。

然而,黑白教条的纯洁性可能是宇宙的死亡。总是存在边缘情况和灰色区域。问题是您开始创建作为 CQS 形式的模式,但不再是纯 CQS。

单子是一种可能性。您可以返回 Monad,而不是您的 Command 返回 void。“void” Monad 可能如下所示:

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

灰色区域的问题在于它很容易被滥用。将诸如新的 OrderID 之类的返回信息放入 Monad 将允许消费者说,“忘记等待事件,我们在这里得到了 ID!!!” 此外,并非所有命令都需要 Monad。您确实应该检查应用程序的结构,以确保您真正达到了边缘情况。

使用 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 暴露一个坏模式,所以你限制了你可以用它做的事情。

于 2017-05-16T19:19:43.593 回答
3

哦,这很有趣。大概我也有话要说。

最近,我一直在使用非正统的 CQS(对于某些人来说可能根本不是 CQS,但我并不在乎)方法,这有助于避免混乱的存储库(因为谁使用规范模式,嗯?)实现和服务随着时间的推移,层类绝对会大幅增长,尤其是在大型项目中。问题是即使其他一切都很好并且开发人员非常熟练,它也会发生,因为(惊讶)如果你有一个大类,它并不总是意味着它首先违反了 SRP。我在此类项目中经常看到的常见方法是“哦,我们有大量的类,让我们划分它们”,这种划分主要是合成的,而不是自然演变的。那么,人们如何应对这种情况呢?他们把一门课做成了几门课。但是当你突然有比以前多几倍的课程时,在一个巨大的项目中使用 DI 会发生什么?不是很漂亮的图片,因为 DI 可能已经充满了注射。因此,出现了外观模式等变通方法(如果适用),其含义是我们: 不要阻止问题;只处理后果并为此花费大量时间;经常使用“综合”的方式进行重构;减少邪恶而不是增加邪恶,但这仍然是邪恶的。

我们该怎么做呢?作为第一步,我们将 KISS 和 YAGNI 应用于 CQS。

  1. 使用 Commands/CommandHandlers 和 Queries/QueryHandlers。
  2. 对包含结果和错误的查询和命令使用通用返回对象(哎哟!)。
  3. 默认情况下避免使用标准服务和存储库实现——仅在绝对必要时。

这种方法解决了哪些问题?

  1. 早期预防代码混乱,更容易使用和扩展(面向未来)。
  2. 信不信由你,对于中等规模的项目,我们根本没有服务类和存储库。项目越大,这种方法就越有利(如果我们假设不需要 CQRS 和 ES,并且仅与标准服务 + 数据层进行比较)。我们对此感到非常满意,因为它对于大多数中型项目的成本和效率来说已经绰绰有余了。

那我建议你怎么做?

  1. 为正确的工作使用正确的工具。使用可以解决您的问题的方法,如果它对您的案例带来不必要的复杂性,“只是因为这就是原因”,请避免按照书本行事。顺便说一句,您多久看到一次完全 RESTful Level 3 API?
  2. 如果您不需要它,尤其是如果您不了解它,请不要使用任何东西,因为如果您真的不使用,则弊大于利。CQRS 适用于某些情况,并且仍然很容易理解,但要付出开发和支持的代价;ES 相当难以理解,甚至更难以构建和支持。
于 2020-10-10T15:17:25.280 回答
2

我喜欢其他人给出的事件驱动架构建议,但我只想提出另一个观点。也许你需要看看为什么你实际上是从你的命令中返回数据。你真的需要它的结果吗,或者如果它失败了你能不能抛出异常?

我并不是说这是一个通用的解决方案,而是切换到更强大的“失败异常”而不是“发回响应”模型帮助我使分离在我自己的代码中真正起作用。当然,你最终不得不编写更多的异常处理程序,所以这是一个折衷方案……但这至少是另一个需要考虑的角度。

于 2010-09-21T23:08:22.957 回答
2

好吧,这是一个很老的问题,但我发布这个只是为了记录。每当您使用事件时,您都可以使用委托。如果您有很多相关方,请使用事件,否则使用回调样式的委托:

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

希望这可以帮助任何迷失的灵魂......

于 2018-08-14T22:00:59.103 回答
1

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.

于 2017-07-18T18:52:14.020 回答
0

CQS 主要用于实现领域驱动设计,因此您应该(正如 Oded 所说)使用事件驱动架构来处理结果。因此,您string result = order.Result;将始终在事件处理程序中,而不是直接在代码中。

查看这篇很棒的文章,它展示了 CQS、DDD 和 EDA 的组合。

于 2010-09-13T15:25:26.073 回答