业务建模:CQRS应用场景

分析了做过的一些项目(基于经典DDD),觉得应用CQRS的场景还是蛮多的,
特别是当出现模块之间出现相互依赖的时候,我这里说的应用场景不是为了保证查询数据的一致性,
而是由领域出发自然而然的过程。
举一个例子:ERP中的物流或供应链管理会有销售模块、采购模块、库存模块、财务模块等,每个模块会有不同的人来负责。
 
就以“销售订单”来举这个例子吧;
 
按照DDD设计,销售人员需要知道当前订单库存的装箱出货情况、财务的开票付款情况,
所以设计上销售订单(在库存上)单向导航的装箱单集合、出库交货单集合(多次装箱、多次出货)、
(在财务上)单向导航到收款单集合与发票集合(多次收款、多次开票)
 
如果销售、库存、财务分开模块设计,销售模块与库存模块和财务模块必然是存在依赖关系,
销售模块依赖于库存模块和财务模块,而库存模块和财务模块不能依赖销售模块,否则会出现交叉引用。
 
现在我想要查询,缺货的订单、已装箱未出库的订单、部分收款已出库、已开票已出库的订单:
把所有的订单明细与装箱单明细、出货单明细、收款单明细、发票明细进行数量、金额比较得过滤出订单,
说到这里有人可能会拍砖了,傻啊,不会在订单上加个库存状态属性、财务状态属性,(有上生产模块的话,再加个生产模块属性)
根据状态来过滤,看上去似乎很美,但问题是谁来更新这些状态?我们模仿一下DDD用一个对话来描述吧:
 
甲: 销售人员?
乙: 销售人员很闲的话可能会去干(老总肯定觉得他真闲得没事干了),否则他们更愿意看到库存、财务作业结果。
甲: 库存人员装箱出货作业完了通知销售人员一下行了,财务有笔款到帐了也通知一下,不就行了。(不通知,销售人员打电话去客户那里问问)
乙: 问题来了,库存、财务怎么知道这是哪个订单对应这个出货与款项了。
甲: 这些单据是根据订单自动生成,上面都有一个订单的订单号,可以根据订单号找到订单更新一下状态,不就好了。
乙: 你怎么知道出库单上关联的那个号码是订单号,它也可能是采购退货单的单号啊?
甲: 我们的编号都有规则订单号是以SO开头的,采购退货单号是以RP开头的,还有一个备注的属性,一目了然。
乙: 你是知道,但你的电脑它知道吗?(这问题问得很傻很天真)
甲: 我都参与了系统的开发,绝对是按照DDD规范来做,基础设施层、领域层、应用层、表现层,多清楚啊。
乙: 你这个判断关联号码的是否是订单号的规则是不是应该写在领域层的库存模块里?是属于领域的一部分?
甲:当然
乙: 那里怎么获得订单并改变它的属性状态的?
甲:是通过订单的服务接口,它是一个领域服务
乙: 这个订单服务接口应该在订单模块了对吧?
甲:当然
乙:销售模块赖于库存模块和财务模块,处于上层,你怎么获得对订单服务借口的引用?
甲:(打开代码……)哦,原来我们把这个判断号码的判断规则写在了应用层,但它应该属于业务的一部分的,怎么把它移到领域层上去了?
乙:你的库存装箱、出货有日志(History)吧,History一个事件集合记录作业过程,
可装箱单或出库交货确认完成时产生发布PickingEvent或DeliveryEvent等
销售订单或采购退货单订阅事件,让他们自己去判断这个单号是否属于自己,修不修该状态就是他们的事,跟库存没有关系。
当然在得在这个事件中带有它们感兴趣的信息(直接把这个聚合对象给他们也可以)
甲:领域事件出现了,那就可以使用CQRS :)
 
--------------------------------------------------------------------------------------
 
以上是我杜撰的一个过程,销售模块也不一定要这样设计,与库存模块、财务模块是可以解偶的,
解偶的话他们之间的联系,也是需要一个协调者或一个事件。
当然了,部分业务逻辑放在应用层,系统也能撮和着用,功能上没问题,只有完美主义者才会较真的。
 
其实,当初碰到这个问题,之前我的处理方案很土
(库存模块定义一个Service接口,在订单模块引用实现并注入容器,库存确认完成的时候获得Service接口执行,
跟领域事件差不多,实现接口实例算是订阅,通过Ioc容器来做发布)
 
这是一个地球人都能明白的例子,从纯业务角度来描述,涉及DDD分层、领域事件、CQRS,希望能对DDD理解有所帮助
个人观点是,领域事件的出现是自然而然的,CQRS应作为一个具体特定场景解决方案。

看得出楼主是位实战经验丰富的大牛。读你的帖子是一种享受。

楼主已经看到了系统模块之间边界重要性,不同领域边界认为可以采取CQRS。通过事件或消息将模块之间进行切分松耦合又不失联系。

在经典解决方案中,还有引入SOA的JMS和ESB服务总线等方式来实现,应该和CQRS两者区别不大,两者都是异步方式,不同的是一个侧重事件,一个侧重消息。

当然,我个人认为CQRS和SOA还有一个本质区别,就是事件或消息的发源地不同,在CQRS中,由领域模型发出领域事件;而在SOA中,是由一个服务发出消息,这两种流派的竞争,就看你的应用是以服务为主,还是状态为主,当然,两者也是可以结合在一起的。

"大牛"不敢当,实践多了,总会有一些体会的 :)


任何“架构”对“领域模型”多少都有影响,
如果是这个影响正是领域“想要的”(正影响),就可以采用,否则(负影响)宁可不用;
采用哪个方案,都能到达目的地,就看你是像坐“火车”、“高铁”还是“飞机”

[该贴被clonalman于2012-08-30 14:30修改过]

2012-08-30 10:11 "@clonalman"的内容
如果是这个影响正是领域“想要的”(正影响),就可以采用,否则(负影响)宁可不用; ...

是啊,架构对领域的影响根据你的经验如何判断?特别是系统开始复杂时,“火车”“飞机”等方式可能影响的是不是效果不同?

同意,做飞机效果是好,但必须承受的负影响是:机票贵、 机场远、提前两个小时到达、飞机上不让打手机等。
如果说“我要到达某个地方,这个地方可能是很远”是领域模型,“飞机”是使用的架构,上面的负影响我根本不在乎,当然是使用“飞机”了;如果负影响是可能会发生“劫机”,那还坐不坐了?

同理,架构对领域的影响(从领域出发)可能是:
在领域层添加DCI里的Context、CQRS里的DomainEvent,如果这些元素是我们领域建模想要的、所期待的,就是正影响,反之,就是负影响。

所以,架构理解应该是领域某个特征自然延伸或实现方式,而不是用一个架构试图解决所有领域问题


[该贴被clonalman于2012-08-30 20:06修改过]

鸟儿有了翅膀能够飞翔,但不能为了飞翔让鸟儿全身长满翅膀

2012-08-30 19:47 "@clonalman"的内容
在领域层添加DCI里的Context、CQRS里的DomainEvent,如果这些元素是我们领域建模想要的、所期待的,就是正影响, ...

基本同意你的观点,可以重新审视一下业务和架构的关系。

这里有一个隐式前提,DDD业务和现有架构脱节,也就是说,现有架构不能很好支撑DDD,为什么这么说呢,因为在这样架构下,DDD实体一直是被操作,作为方法参数传来传去,见下面伪代码演示:

public void myMethod(Entity entity){
.....
}

然后我们会有出现另外一个方法类似上面,只是其中方法代码有稍微不同:

public void myMethod2(Entity entity){
.....
}

public void myMethod3(Entity entity){
.....
}

问题来了,由于这三个方法中有一部分是共同的,当我们修改一个方法时,另外两个都要修改,万一忘记修改,就出现BUG,这时我们很容易想把三个方法中共同部分抽象成一个方法,这里有两个方向:一个是首先用继承模板,这是坏的设计,为什么坏这里不多说。

第二个是:之前我们总是从功能行为角度考虑实现,换个不同角度,从实体角度考虑,这共同的部分是不是可以写入实体内部呢? 也许从业务上讲,属于实体的行为,属于实体的职责,实体应该自己干的事情,应该有责任去做的事情,当然这其中也区分为基本职责和业务职责,后者我们通过DCI来实现。

下面问题来了,实体代码变为:
public class Entity{

public void myMethodCommon(){
......
}
}

如果myMethodCommon方法需要调用存储数据库,或调用服务或调用其他实体交互怎么办?

那么一般就是把这些资源注入到实体中,实体代码为:
public class Entity{

//数据库资源
public MyRepository myRepository;

//其他服务....
public MyService myService;

public void myMethodCommon(){
......
}
}

很明显,架构技术因素侵入搞脏了实体,怎么解决这个问题?

联想到Command命令模式,既然用户通过界面可以向服务以命令模式发出各种调用(如Struts等框架),实体作为用户行中模型的代表,代表用户需求思想,为什么不可以以命令对技术架构发出各种调用呢?通过命令模式,实体将各种调用打包成请求,而我们这时需要做的只是在实体中提供一个传递命令管道:
public class Entity{

public SubPublisherRoleIF subPublisherRole;

public void myMethodCommon(){
......
subPublisherRole.send...
}

}
这样,基本杜绝了实体和技术架构如各种仓储或服务的依赖,当然,更好的方式是,连命令管道都没有,直接在运行是注入各种实体需要的资源,这种方式虽然很干净,好像一点副作用都没有,但是带来另外问题,如变魔术,代码本来是让大家读懂的脚本,这时反而变得不容易理解,因为看到的代码和实际运行是完全不同的。

总是,正如如Clonalman所说,架构对领域总是有各种副作用,关键是我们能够结束哪种,可能完美是不存在的。

一个很困惑难以理解的想象,人类的认识总是想寻求一个一劳永逸的解决办法,
比如是“ET”或“神仙姐姐”干的,逻辑思维很合理和完美,
但问题“ET”与“神仙姐姐”符合现实吗?

把仓储或服务注入实体,这是DDD明确反对的,领域上也是不可理解的。
这么干的原因往往是偷懒或着对领域理解不够深刻造成的。

持久化:
持久化自身:不应放在(myMethodCommon中)
EntityRepository repository = new EntityRepository();
repository.save(entity);
(如果在方法中实现自身持久化,你是使用ActiveRecord模式)
持久化其他(Entity2):意味着Entity与Entity2存在某种业务联系,Entity2的生命周期是否为Entity所决定


public class Entity{

public Entity2Collection Entity2s = new Entity2Collection();

public void myMethodCommon(){
//持久化
Entity2 entity2 = new Entity2();
....
Entity2s.add(Entity2); <-- Entity2Collection.add 如何保存持久化,这是发挥架构的威力的地方

}

}

服务调用:当我们想在一个方法中调用服务来,
意味领域上往往Service内对象(Entity3)并不想Entity知道它是谁、是怎么工作?(也许Entity也并不想知道)
如果觉得实体里需要调用Service的地方,其方法myMethodCommon本身就是领域服务EntityService的一个方法。

public class EntityPubService <--因为banq没给具体场景,这个“pub”是举例用的,实际应该领域里的一个动作
{
EntityBusService bus; <- 这个可能是一个设施服务层的服务,是架构发挥作用的地方。。。。

public void process(Entity entity) <-参数不一定直接使用Entity,可以是其他与Entity相关的
{
bus.send(entity);
}
}

public class EntitySubService <--因为banq没给具体场景,这个“sub”是举例用的,实际应该领域里的一个动作
{
public void process(Entity entity)
{
Entity3Repository repository = new Entity3Repository();
Entity3 entity3 = repository.find(1); ///(随便写的1)
entity3.status = 1;
entity.reference = entity3.id
.......
}
}


myMethod、myMethod2、myMethod3的公共部分如果用到了EntityService的方法,
说明myMethod、myMethod2、myMethod3很可能是一个领域服务的方法,
Entity4Service.myMethod
Entity5Service.myMethod2
Entity6Service.myMethod3
否则可以是一个实体的方法如
Entity4.myMethod
Entity5.myMethod2
Entity6.myMethod3


是否属于领域方法还是实体方法,可能是动态,是业务本身决定的,
如果哪天Entity4.myMethod需要的添加一个服务来配合,就需要重构

实体:
public class Entity4
{
public void myMethod(Entity entity){
entity.myMethodCommon();
}
}

public class Entity5
{
public void myMethod2(Entity entity){
entity.myMethodCommon();
}
}

public class Entity6
{
public void myMethod3(Entity entity){
entity.myMethodCommon();
}
}

public class Entity4Serivce
{
EntityPubService entityPubService; <- 发挥架构威力(Ioc注入)

public void myMethod(Entity entity){
entityPubService.process(entity);
entity.myMethodCommon();
}
}

public class Entity5Service
{
EntityPubService entityPubService; <- 发挥架构威力(Ioc注入)

public void myMethod2(Entity entity){
entityPubService.process(entity);
entity.myMethodCommon();
}
}

public class Entity6Service
{
EntityPubService entityPubService; <- 发挥架构威力(Ioc注入)
public void myMethod3(Entity entity){
entityPubService.process(entity);
entity.myMethodCommon();
}
}

如果Entity是一个Domain Event,EntityBusService可能就会选择CQRS来实现,
你选择SOA或EJB服务总线也可以,只是“火车”或“飞机”的区别而已。(哪天我可以介绍具体场景的可能有助进一步理解)
(服务的注入我都舍不得用标签,而是根据类名称,比如一Service结尾就认为是领域服务,即符合DDD规范,又干干净净)
(架构最好能做到“随风潜入夜,润物细无声”)
[该贴被clonalman于2012-08-31 10:22修改过]

当然,引入一个Role也没问题,但它为解决问题能是我们想象出来的“ET外星人”或“神仙姐姐”,并强加给领域。。。

把一部“记录片”拍成“科幻片”或“神话片”

以上我自己的理解,个人观点。。。

2012-08-31 10:02 "@clonalman"的内容
当然,引入一个Role也没问题,但它为解决问题能是我们想象出来的“ET外星人”或“神仙姐姐”,并强加给领域 ...

说得很有趣,我们现在要解决的问题是让实体模型直接指挥架构,看来只有Scala 的trait或ruby的Mixin符合你的润物无声的要求。我也比较认同,但是担心问题还是两面的,因为润物无声是隐式的,无法显式,那么是否类似魔术Magic,对于代码结构阅读理解是不是有些障碍?

“Mixin”在代码阅读上肯定是有障碍的,但它脚踏实地的解决方法,
一个对象属性方法在运行时再做Mixin,只是技术层面解决方案,
在领域层很也是难理解的,负影响也不小
[该贴被clonalman于2012-08-31 11:47修改过]
[该贴被clonalman于2012-08-31 11:57修改过]

2012-08-31 11:31 "@clonalman"的内容
在领域层很也是难理解的,负影响也不小 ...

嗯,那么我们看看scala,一般使用Scala的Actor模型来实现实体,Actor与外界交互都是消息,类似事件,这也是来自ERlang的并发模型,但是需要实体extends继承一下Actor(这与我们引入角色字段类似)。

所以,是否可以得出,领域和架构因为本来是正交的两件事,要将它们完美交合在一起,焊点总会看到? 呵呵。

主要是做实践的,理论上也没太多时间考虑,
Scala这个体系不是很了解(没时间研究)。:(
(实体直接继承Actor这个不好看吧,现在不都强调Pure Object)

领域与架构要找到一个结合点的话,
Bounded Context或Repository应该是再合适不过的了,用它来屏蔽技术细节

[该贴被clonalman于2012-08-31 12:32修改过]

Bounded Context是一个好的角度,不知你有无这方面实践分享一下。

再回到CQRS应用场景,CQRS和Event Source是密切相关的,ES属于CQRS中的Command部分,MartinFowler的ES文章谈到,将导致状态变化的所有事件提取出来。

比如我们原来没有在业务上引入考虑事件这个角度:

上面是典型的SSH或javaEE实现的方式,一个服务一个实体,而引入事件之后,如下图:

那么正如大部分系统都可以引入服务这个概念,“服务”这个概念既是业务也是技术概念,成为业务和技术架构的桥梁,谈到服务,业务人员知道它在业务上代表什么,技术人员知道如何实现它。

而“事件”也是一种和“服务”类似的介于技术和业务之间桥梁的术语,通过引入事件,应该不会对业务领域包括实体有伤害作用,也不会感觉它是天外来客或神仙姐姐,否则服务这个概念也应该是了。

不知上面观点是否认可?呵呵。

相关讨论:
业务建模: CQRS是鸡肋?

数据Data 上下文Context 交互Interaction(DCI):面向对象范式的演进
[该贴被banq于2012-08-31 16:07修改过]

完全同意,领域内的任何对象都可以是技术概念,而技术的概念不一定是领域的概念!

领域上“需要记录船经过的港口”与技术上Event Sourcing,刚好重叠而已
如果领域需求根本不许要记录船经过的港口,ShippingEvent根本就没有必要,
也没有必要使用CQRS,Event Shourcing(个人观点)