当命令需要结果数据时,如何应用命令查询分离(CQS)?

时间:2010-09-10 20:21:40

标签: c# command-query-separation

在维基百科对command query separation的定义中,陈述

  

更正式地说,方法应该只返回一个值   如果它们是引用透明的   因而没有副作用。

如果我发出命令,我应该如何确定或报告该命令是否成功,因为通过此定义,该函数无法返回数据?

例如:

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

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

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

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

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

我在这里错过了什么吗?

谢谢!

注意: Martin Fowler对此有关cqs CommandQuerySeparation的限制的说法:

  

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

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

10 个答案:

答案 0 :(得分:39)

这个问题虽然陈旧但尚未收到令人满意的答案,所以我将在近一年前对我的评论进行详细阐述。

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

让我们以您的购买为例。 StoreService.ProcessPurchase将是处理购买的合适命令。这将生成PurchaseReceipt。这是一种更好的方式,而不是在Order.Result中返回收据。为了使事情变得非常简单,您可以从命令返回收据并在此处违反CQRS。如果您想要更清晰的分离,该命令将引发您可以订阅的ReceiptGenerated事件。

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

答案 1 :(得分:17)

答案 2 :(得分:11)

我在CQS&amp ;;之间看到了很多困惑。 CQRS(正如Mark Rogers在一个答案中所注意到的那样)。

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

CQS是应用程序任何部分代码级别的良好编程原则。不只是域名区域。该原则存在的方式比DDD(和CQRS)长。它表示不会弄乱使用只返回数据的查询来更改应用程序的任何状态的命令,并且可以在不更改任何状态的情况下随时调用。 在我以后的Delphi中,lan​​quage显示了功能和程序之间的差异。编码'功能程序被认为是一种不好的做法。正如我们给他们打电话的那样。

回答问题: 人们可以想办法解决执行命令并获取结果的方法。例如,通过提供具有void execute方法和readonly命令结果属性的命令对象(命令模式)。

但坚持CQS的主要原因是什么? 保持代码可读性和可重用性,而无需查看实现细节。您的代码应该值得信赖,不会造成意外的副作用。 因此,如果命令想要返回结果,并且函数名称或返回对象清楚地表明它是带有命令结果的命令,那么我将接受CQS规则的例外。不需要让事情变得更复杂。 我同意Martin Fowler(如上所述)。

顺便说一下:严格遵循这条规则会打破整个流畅的api原则吗?

答案 3 :(得分:2)

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

我并不是说这是一个通用的解决方案,但是切换到更强大的“失败例外”而不是“发回响应”模型帮助我在分离实际上在我自己的代码中工作了很多。当然,那么你最终还是要编写更多的异常处理程序,所以这是一个权衡......但它至少是另一个需要考虑的角度。

答案 4 :(得分:2)

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

答案是:你没有。如果要运行命令并返回结果,则表示您没有使用CQS。

然而,黑白教条的纯洁可能是宇宙的死亡。总有边缘情况和灰色区域。问题是你开始创建一种CQS形式的模式,但不再是纯粹的CQS。

Monad是一种可能性。你可以返回Monad而不是你的Command返回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; }
}

现在您可以使用“Command”方法:

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

灰色区域的问题是容易被滥用。将新订单ID等返回信息放入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暴露出一个糟糕的模式,所以你限制你可以做什么它

答案 5 :(得分:2)

好吧,这是一个非常老的问题,但我仅出于记录目的发布此问题。 无论何时使用事件,都可以使用委托。如果您有很多感兴趣的参与者,请使用事件,否则请使用回调样式的委托:

redirect

对于操作失败的情况,您还可以设置一个阻止

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

这降低了客户端代码的循环复杂性

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

希望这可以帮助任何迷路的人...

答案 6 :(得分:2)

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

最近一段时间,我一直在使用非正统的CQS(也许对某人根本不是CQS,但我并不在乎)这种方法有助于避免混乱的存储库(因为谁使用了规范模式,是吧?)实现和服务层类,它们会随着时间的推移而极大地增长,尤其是在大型项目中。问题是,即使其他一切都很好并且开发人员也很熟练,它还是会发生,因为(惊奇)如果您的班级很大,并不总是意味着它首先违反了SRP。我在此类项目中经常看到的常见方法是“哦,我们有很多类,让我们划分它们”,这种划分主要是综合性的,而不是自然发展的。那么,人们如何应对呢?他们从一个班里选出几个班。但是,当您突然有比以前多几倍的班级时,在一个大型项目中使用DI会发生什么?效果不是很好,因为DI可能已经充满了注入。因此,有一些解决方法,例如立面图案等(适用时),其含义是:只处理后果并为此花费很多时间;经常使用“合成”方法进行重构;得到更少的邪恶而不是更多的邪恶,但这仍然是邪恶。

我们该怎么办?第一步,我们将KISS和YAGNI应用于CQS。

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

这种方法可以解决什么问题?

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

那我建议你做什么?

  1. 使用正确的工具完成正确的工作。使用解决您的问题的方法,如果您的案例带有不必要的复杂性(正因为如此),则避免使用本书中的所有内容。顺便问一下,您多久看到一次完整的RESTful 3级API?..
  2. 如果不需要,请勿使用任何东西,尤其是如果您不理解的话,因为如果您确实不这样做,则弊大于利。 CQRS在某些情况下是很好的,并且仍然很容易理解,但是要付出开发和支持的代价。 ES很难理解,甚至很难构建和支持。

答案 7 :(得分:1)

我到目前为止已经很晚了,但还有一些未提及的选项(但不确定它们是否真的那么棒):

我之前没有见过的一个选项是为命令处理程序创建另一个接口来实现。也许命令处理程序实现ICommandResult<TCommand, TResult>。然后,当正常命令运行时,它将结果设置在命令结果上,然后调用者通过ICommandResult接口提取结果。使用IoC,您可以使其返回与命令处理程序相同的实例,以便您可以将结果拉回来。但是,这可能会打破SRP。

另一种选择是使用某种共享Store,它允许您以Query随后可以检索的方式映射命令结果。例如,假设您的命令有一堆信息,然后有一个OperationId Guid或类似的东西。当命令完成并获得结果时,它将答案推送到具有该OperationId Guid作为键的数据库或另一个类中的某种共享/静态字典。当调用者返回控制权时,它会根据给定的Guid调用Query以根据结果撤回。

最简单的答案是将结果推送到Command本身,但这可能让某些人感到困惑。我看到的另一个选项是事件,你可以在技术上做,但如果你在网络环境中,这使得处理起来要困难得多。

修改

在使用了这个之后,我最终创建了一个“CommandQuery”。显然,它是命令和查询之间的混合体。 :)如果您需要此功能,则可以使用它。但是,需要有充分的理由这样做。它不会重复,也不能缓存,因此与其他两个相比存在差异。

答案 8 :(得分:1)

花更多时间考虑为什么要使用命令查询分隔。

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

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

因为仅出于

的目的创建单独的查询会很浪费

确定先前的命令是否正常工作。

这样的事情还可以

我的书:

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

您的示例的缺点是

返回的内容不明显

方法。

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

目前尚不清楚“结果”到底是什么。

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

类似于以下内容:

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

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}

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

答案 9 :(得分:0)

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

查看this great article,其中显示了CQS,DDD和EDA的组合。