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

在维基百科的命令查询分离定义中,有人说   更正式地说,方法应该只返回一个值   如果它们是引用透明的   因此没有副作用。 如果我发出命令,我该如何确定或报告该命令是否成功,因为通过此定义,该函数无法返回数据? 例如:
string result = _storeService.PurchaseItem(buyer, item);
此调用中包含命令和查询,但查询部分是命令的结果。我想我可以使用命令模式重构这个,如下所示:
PurchaseOrder order = CreateNewOrder(buyer, item);
_storeService.PerformPurchase(order);
string result = order.Result;
但这似乎增加了代码的大小和复杂性,这对于重构来说并不是一个非常积极的方向。 当您需要操作结果时,有人能给我一个更好的方法来实现命令查询分离吗? 我在这里错过了什么吗? 谢谢! 笔记: Martin Fowler对此有关cqs CommandQuerySeparation的限制:   Meyer喜欢使用命令查询   绝对分离,但有   例外。弹出堆栈是一件好事   修改的修饰符示例   州。梅耶正确地说你   可以避免使用这种方法,但它   是一个有用的成语。所以我更喜欢   我尽可能遵循这个原则,但是   我准备打破它以获得我的   流行。 从他的观点来看,重构命令/查询分离几乎总是值得的,除了一些小的简单例外。     
已邀请:
这个问题已经过时了,但还没有得到令人满意的答案,所以我将在近一年前对我的评论进行详细阐述。 使用事件驱动的体系结构非常有意义,不仅可以实现清晰的命令/查询分离,还因为它打开了新的体系结构选择,并且通常适合异步编程模型(如果需要扩展体系结构,则非常有用)。通常情况下,您会发现解决方案可能在于对您的域建模有所不同。 所以让我们以您的购买为例。
StoreService.ProcessPurchase
是处理购买的合适命令。这会产生一个
PurchaseReceipt
。这是一种更好的方式,而不是在
Order.Result
中返回收据。为了使事情变得非常简单,您可以从命令返回收据并在此处违反CQRS。如果您想要更清晰的分离,该命令将引发您可以订阅的
ReceiptGenerated
事件。 如果你考虑你的域名,这实际上可能是一个更好的模型。当您在收银台结账时,您可以按照此流程进行操作。在您的收据生成之前,可能需要进行信用卡检查。这可能需要更长时间。在同步场景中,您将在收银台等待,无法执行任何其他操作。     
这些链接可能有帮助 同时......在我的架构的命令方面 从命令处理程序返回数据 同时......在我的架构的查询方面 而这......     
我在CQS&amp ;;之间看到了很多混乱。 CQRS(正如Mark Rogers在一个答案中所注意到的那样)。 CQRS是DDD中的体系结构方法,在查询的情况下,您不会从聚合根及其所有实体和值类型构建完整的对象图,而只是在列表中显示的轻量级视图对象。 CQS是应用程序任何部分代码级别的良好编程原则。不只是域名区域。该原则存在的方式比DDD(和CQRS)长。它表示不会弄乱使用只返回数据的查询来更改应用程序的任何状态的命令,并且可以在不更改任何状态的情况下随时调用。 在我以后的Delphi中,lan​​quage显示了功能和程序之间的差异。编码'functionprocedures'被认为是一种不好的做法,因为我们将它们调回来也是如此。 回答问题: 人们可以想办法解决执行命令并获取结果的方法。例如,通过提供具有void execute方法和readonly命令结果属性的命令对象(命令模式)。 但是坚持CQS的主要原因是什么? 保持代码可读性和可重用性,而无需查看实现细节。您的代码应该值得信赖,不会造成意外的副作用。 因此,如果命令想要返回结果,并且funcion名称或返回对象清楚地表明它是带有命令结果的命令,我将接受CQS规则的例外。不需要让事情变得更复杂。 我同意Martin Fowler(如上所述)。 顺便说一下:不会严格遵循这条规则打破整个流畅的api原则吗?     
我喜欢其他人给出的事件驱动架构建议,但我只想提出另一种观点。也许您需要查看为什么实际上从命令返回数据。你真的需要它的结果吗,或者如果它失败了你可以逃避抛出异常吗? 我并不是说这是一个通用的解决方案,但是切换到更强大的“失败例外”而不是“发回响应”模型帮助我在分离实际上在我自己的代码中工作。当然,那么你最终还是要编写更多的异常处理程序,所以这是一个权衡......但它至少要考虑另一个角度。     
问题是;当您需要命令结果时如何应用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中意外暴露出一个糟糕的模式,所以你限制你可以用它来做什么。     
嗯,这是一个非常古老的问题,但我发布这个仅仅是为了记录。 无论何时使用事件,您都可以使用委托。如果您有许多感兴趣的参与方,请使用事件,否则使用回调样式的委托:
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 => {...});
希望这可以帮助那些迷失的灵魂......     
CQS主要用于实现域驱动设计,因此您应该(如Oded所述)使用事件驱动架构来处理结果。因此,您的
string result = order.Result;
将始终位于事件处理程序中,而不是之后直接在代码中。 看看这篇很棒的文章,它展示了CQS,DDD和EDA的组合。     
我已经很晚了,但还有一些没有被提及的选项(但是,不确定它们是否真的那么棒): 我之前没有见过的一个选项是为命令处理程序创建另一个接口来实现。也许
ICommandResult<TCommand, TResult>
命令处理程序实现。然后,当正常命令运行时,它将结果设置在命令结果上,然后调用者通过ICommandResult接口提取结果。使用IoC,您可以使其返回与命令处理程序相同的实例,以便您可以将结果拉回来。虽然,这可能会打破SRP。 另一种选择是使用某种共享存储,它允许您以Query随后可以检索的方式映射命令结果。例如,假设您的命令有一堆信息,然后有一个OperationId Guid或类似的东西。当命令完成并获得结果时,它将答案推送到具有该OperationId Guid作为键的数据库或另一个类中的某种共享/静态字典。当调用者返回控制权时,它会根据给定的Guid调用Query以基于结果撤回。 最简单的答案是将结果推送到命令本身,但这可能会让某些人感到困惑。我看到的另一个选项是事件,你可以在技术上做,但如果你在一个网络环境中,这使得处理起来要困难得多。 编辑 在使用了这个之后,我最终创建了一个“CommandQuery”。显然,它是命令和查询之间的混合体。 :)如果您需要此功能,则可以使用它。但是,需要有充分的理由这样做。它不会重复,也不能缓存,因此与其他两个相比存在差异。     
花点时间考虑一下你想要Command Query Separation的原因。 “它可以让您随意使用查询,而无需担心更改系统状态。” 因此,从命令返回一个值以让调用者知道它成功是可以的 因为单独创建一个单独的查询是浪费的 找出以前的命令是否正常工作。这样的事情还可以 我的书:
boolean succeeded = _storeService.PurchaseItem(buyer, item);
你的例子的一个缺点是,你的回归并不明显 方法。
string result = _storeService.PurchaseItem(buyer, item);
目前尚不清楚“结果”究竟是什么。 使用CQS(命令查询分隔)可以让事情变得更加明显 类似于以下内容:
if(_storeService.PurchaseItem(buyer, item)){

    String receipt = _storeService.getLastPurchaseReciept(buyer);
}
是的,这是更多的代码,但更清楚的是发生了什么。     

要回复问题请先登录注册