当一个命令需要结果数据时,如何应用命令查询分离(CQS)?
在维基百科的命令查询分离的定义,这是说
更正式的说,方法只有在它们具有透明性并且没有副作用的情况下才能返回价值。
如果我发出一个命令,我应该如何确定或报告该命令是否成功,因为通过这个定义该函数不能返回数据?
例如:
string result = _storeService.PurchaseItem(buyer, item);
这个调用有一个命令和查询,但查询部分是命令的结果。 我想我可以重构这个使用命令模式,如下所示:
PurchaseOrder order = CreateNewOrder(buyer, item); _storeService.PerformPurchase(order); string result = order.Result;
但是,这似乎是在增加代码的大小和复杂性,这不是一个非常积极的重构方向。
有人可以给我一个更好的方法来实现命令 – 查询分离,当你需要一个操作的结果吗?
我在这里错过了什么?
谢谢!
注意:Martin Fowler对cqs CommandQuerySeparation的限制有这个说法:
Meyer绝对喜欢使用命令 – 查询分离,但也有例外。 popup堆栈是修改状态的修改器的一个很好的例子。 迈尔正确地说,你可以避免使用这种方法,但它是一个有用的习惯用法。 所以我更喜欢遵循这个原则,但我准备打破它,让我的stream行。
从他的angular度来看,除了一些小的简单例外之外,几乎总是要对命令/查询分离进行重构。
这个问题虽然陈旧,但还没有得到令人满意的答案,所以我将在近一年前对我的评论做一些阐述。
使用事件驱动的体系结构非常有意义,不仅可以实现清晰的命令/查询分离,还可以打开新的体系结构select,并且通常适合asynchronous编程模型(如果需要扩展架构,则可用)。 通常情况下,您会发现解决scheme可能在于对域进行不同的build模。
那么让我们来看看你的购买例子。 StoreService.ProcessPurchase
将是处理购买的合适命令。 这将生成一个PurchaseReceipt
。 这是一个更好的方式,而不是在Order.Result
中返回收据。 为了使事情变得非常简单,您可以从命令中返回收据,并在这里违反CQRS。 如果您想要更清晰的分隔,则该命令会引发您可以订阅的ReceiptGenerated
事件。
如果你考虑你的域名,这可能是一个更好的模式。 当你在出纳员结账时,你要遵循这个过程。 在您的收据产生之前,信用卡支票可能到期。 这可能需要更长的时间。 在同步的情况下,你会在收银台等待,无法做任何事情。
这些链接可能有帮助
- 同时…在我的架构的命令方面
- 从命令处理程序返回数据
- 同时…在我的架构的查询方面
- 也是这个…
我在CQS和CQRS之间看到了很多混乱(正如马克·罗杰斯在一个答案中注意到的那样)。
CQRS是DDD中的一种体系结构方法,在查询的情况下,您不会从具有所有实体和值types的聚合根构build完整的对象图,而只是在列表中显示轻量级视图对象。
在您的应用程序的任何部分,CQS都是代码级的一个很好的编程原则。 不仅仅是域名。 原理存在的方式比DDD(和CQRS)更长。 它说,不要搞乱改变任何应用程序状态的查询,只是返回数据,可以随时调用,而不会改变任何状态。 在我以前用delphi的时候,这个窍门显示了function和程序之间的区别。 我们称之为“function程序”的代码被认为是不好的做法。
为了回答所问的问题:可以想办法解决执行命令和取回结果的问题。 例如,通过提供具有无效执行方法和只读命令结果属性的命令对象(命令模式)。
但是坚持CQS的主要原因是什么? 保持代码的可读性和可重用性,而无需查看实现细节。 您的代码应该值得信赖,不要导致意想不到的副作用。 所以如果命令要返回一个结果,并且funcion名称或者返回对象清楚地表明它是一个带有命令结果的命令,那么我将接受CQS规则的exception。 没有必要让事情更复杂。 我同意Martin Fowler(上文提到)在这里。
顺便说一下:不会严格遵循这个规则打破整个stream利的API原则?
我喜欢其他人给出的事件驱动架构build议,但我只是想抛出另一个观点。 也许你需要看看为什么你实际上从你的命令返回数据。 你真的需要结果吗?或者如果失败就抛出exception呢?
我并不是说这是一个通用的解决scheme,但是切换到更强大的“失败的例外”,而不是“发送回应”模式,这使我在代码中实际工作起了很大的作用。 当然,那么你最终不得不写更多的exception处理程序,所以这是一个权衡…但至less从另一个angular度来考虑。
CQS主要用于实现领域驱动devise,因此您应该(如Oded也指出)使用事件驱动架构来处理结果。 你的string result = order.Result;
因此总是处于事件处理程序中,而不是直接在代码中。
看看这个伟大的文章 ,显示CQS,DDD和EDA的组合。
问题在于; 当你需要命令的结果时,你如何申请CQS?
答案是:你不。 如果你想运行一个命令并得到一个结果,你不使用CQS。
然而,黑色和白色的教条纯洁可能是宇宙的死亡。 总是有边缘情况和灰色地带。 问题是你开始创buildCQSforms的模式,但不再是纯粹的CQS。
Monad是一种可能性。 而不是你的命令返回void,你可以返回Monad。 一个“无效的”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(); }
灰色地带的问题是很容易被滥用。 将返回信息(例如新的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有一点灰色区域,您就可以拥有所需的所有function和正确性,但要确保您不会意外地通过Monad暴露出不好的模式,因此限制了您可以使用的方式。
我真的很迟到,但还有一些没有提到的选项(虽然不知道它们是否真的很棒):
我之前从未见过的一个选项是为命令处理程序创build另一个接口来实现。 也许是命令处理程序实现的ICommandResult<TCommand, TResult>
。 然后当正常的命令运行时,它将结果设置在命令结果上,然后调用者通过ICommandResult接口抽出结果。 使用IoC,你可以使它返回与Command Handler相同的实例,所以你可以将结果返回。 虽然,这可能会打破SRP。
另一个select是拥有某种共享存储,让您以某种查询可以检索的方式映射命令的结果。 例如,说你的命令有一堆信息,然后有一个OperationId Guid或类似的东西。 当命令完成并获得结果时,它会将该操作作为键或其他类中的某种共享/静态字典推送给数据库。 当调用者重新获得控制权时,它会根据给定的Guid调用一个基于结果的查询。
最简单的答案就是把这个结果推到Command上去,但这可能会让一些人感到困惑。 我提到的另一个选项是事件,你可以在技术上做,但如果你在一个networking环境,这使得它更难处理。