我一直在修改穷人的CQRS 1,因为我喜欢它的灵活性,可以在一个数据存储中存储细粒度的数据,这为分析提供了极大的可能性,从而增加了商业价值,并且在需要时还提供了另一种包含非规范化数据的读取以提高性能。 。
但是不幸的是,从一开始,我就一直在为应该在这种架构中放置业务逻辑的问题而苦苦挣扎。
据我了解,命令是传达意图的手段,它本身与域没有关系。它们基本上是数据(哑巴,如果需要的话)传输对象。这是为了使命令可以在不同技术之间轻松转移。对于成功完成的事件的响应,同样适用于事件。
在典型的DDD应用程序中,业务逻辑驻留在实体,值对象,集合根中,它们既包含数据又包含行为。但是命令不是域对象,因此命令不应仅限于数据的域表示形式,因为这会对它们造成很大的压力。
因此,真正的问题是:逻辑到底在哪里?
我发现我在尝试构建一个非常复杂的集合(该集合为它的值的组合设置一些规则)时往往最容易遇到这种斗争。另外,在对域对象建模时,我喜欢遵循快速失败范例,知道对象何时到达处于有效状态的方法。
假设聚合Car
使用两个组件:
Transmission
,Engine
。
两个Transmission
和Engine
值对象被表示为超级类型和具有根据子类型,Automatic
和Manual
传输,或Petrol
与Electric
分别引擎。
在此域中,完全依靠成功创建的Transmission
,,Automatic
或Manual
或任意一种类型来生存Engine
。但是Car
聚合引入了一些新规则,这些规则仅在Transmission
和Engine
对象在同一上下文中使用时才适用。即:
- 当汽车使用
Electric
发动机时,唯一允许的变速箱类型是Automatic
。 - 当汽车使用
Petrol
发动机时,可能有两种类型的发动机Transmission
。
我可以在创建命令的级别上捕获这种违反组件组合的行为,但是正如我之前所说,据我所知,不应这样做,因为命令将包含业务逻辑,而业务逻辑应限于域层。
一种选择是将这种业务逻辑验证移至命令验证器本身,但这似乎也不对。感觉就像我将解构该命令,检查使用getter检索的属性,并在验证器中比较它们并检查结果。这让我尖叫得像是违反了得墨meter耳的法律。
放弃提到的验证选项,因为它似乎不可行,似乎应该使用该命令并从中构造聚合。但是这种逻辑应该在哪里存在?是否应该在负责处理具体命令的命令处理程序中?还是应该在命令验证器中(我也不喜欢这种方法)?
我当前正在使用命令,并在负责的命令处理程序中从中创建一个聚合。但是,当我执行此操作时,如果我有一个命令验证器,它将根本不包含任何内容,因为如果该CreateCar
命令存在,则它将包含我知道在单独的情况下有效的组件,但集合可能表示不同。
让我们想象一个混合了不同验证过程的场景-使用CreateUser
命令创建一个新用户。
该命令包含Id
将要创建的一个用户及其Email
。
系统为用户的电子邮件地址规定以下规则:
- 必须是唯一的
- 不能为空,
- 最多包含100个字符(数据库列的最大长度)。
在这种情况下,即使有一个唯一的电子邮件是一条业务规则,但对其进行汇总检查几乎没有任何意义,因为我需要将系统中的全部当前电子邮件加载到内存中,并在命令中检查该电子邮件(Eeeek!某物,某物,性能)。因此,我将此检查移至命令验证程序,该程序将UserRepository
作为依赖项,并使用存储库来检查是否存在使用命令中存在电子邮件的用户。
当涉及到这一点时,突然将其他两个电子邮件规则也放入命令验证器中是有意义的。但是我觉得规则应该确实存在于User
聚合中,并且命令验证程序应该只检查唯一性,如果验证成功,我应该继续在中创建User
聚合CreateUserCommandHandler
并将其传递到要保存的存储库中。
我之所以这样,是因为存储库的save方法很可能会接受一个聚合,该聚合可确保一旦传递了聚合,所有不变量都将得到满足。当逻辑(例如,非空性)仅出现在命令验证本身中时,另一位程序员可以完全跳过此验证,并直接UserRepository
使用User
对象调用对象中的save方法,这可能导致致命的数据库错误,因为电子邮件可能包含太久了。
您个人如何处理这些复杂的验证和转换?我大多对自己的解决方案感到满意,但是我觉得我需要肯定的是,我对自己的选择并不满意,但我的想法和方法并不完全愚蠢。我完全接受完全不同的方法。如果您有自己尝试过的东西并且为您做得很好,我很乐意看到您的解决方案。
1作为负责创建RESTful系统的PHP开发人员,我对CQRS的解释与标准的异步命令处理方法略有不同,例如有时由于需要同步处理命令而从命令返回结果。
CommandDispatcher
。