领域模型的行为设计

领域模型的行为设计是面向对象领域建模设计的重要部分。

在没有设计的朴素的情况下,领域模型一般是一个数据对象(DTO等),其中只有setter/getter方法,是一种纯粹的数据结构,然后将很多数据结构的算法操作设计在Service等专门接口类中。这样,数据对象作为服务接口方法的参数传入,在服务的方法中被加工。如下代码:


//失血模型 贫血模型
public class A{
private int id;
...
//只有setId(int id) 和getId()方法
}

public class AServiceImp implements AService{
//失血模型作为方法参数传入,被操作
public void createA(A a){
...
}

}

而DDD领域驱动设计告知我们要注重领域模型的业务方法设计,领域模型=数据结构+操作方法,才是一个完整的真正对象,也才能够真正发挥对象封装的作用。

但是一个模型对象可能有很多方法,哪些方法应该作为对象本身的方法?哪些方法又应该依赖其他对象进行?举例:


public class A{

//对象本身独立行为
public void thisIsMyMethod(){
....
}

//依赖其他对象的交互行为
public void replyOthers(B b){
b.xxxx();
..
}

}

我们把A对象自身固有行为看成是A的一种能力,而把需要依赖其他对象的方法称为交互行为。哪些属于A的自身方法?哪些属于交互方法?设计思路和方法是如何考虑的?

这种常见的貌似非常简单的问题其实不简单,而单纯依靠几个设计模式并不能解决。

还有在不同场景下,有时A对象依赖B对象,同时A依赖C对象,有时只依赖B对象,如何反应这种应场景变化而导致交互方式的不同?

如下图:
[该贴被admin于2013-04-23 07:39修改过]



第一个思路: DCI,根据不同场景,将其对应的角色职责动态注入数据模型中。

这种方法认为,对象的行为都是其在一定场景下扮演某个角色才具备的,因此,先将行为设计到相应角色对象中,然后在需要时,将某个角色与数据对象混合mixin。

这种方法的特点非常类似依赖注入,是两个对象的合并组合,类似桥模式。



[该贴被admin于2013-04-22 20:32修改过]


2. DDD聚合根思路,先去除不必要的关联依赖,找出高聚合,比如结合业务发现,A和B是代表各自聚合的实体根。切割后分别设计,聚合根实体对象的行为应该是保证对象内部状态一致性的那些动作。

所谓逻辑一致性,也就是业务的规则 约束或校验,以日常例子说明,如果一群人的观点一致,那么我们就可以用XX组织 XX帮派来称呼他们,人以群分,物以类群,人或物因为有内部一致性才归类。

具体来说,类似状态模式,如果当前进入播放状态(假设有开始 播放 暂停 停止四个状态),那么下一个状态只可能是暂停或停止状态,肯定不是开始状态,那么这种一致性判断在什么时候什么地方判断呢?

很显然应该是在触发状态改变之前的动作行为中判断,那么这些动作就不能放在领域模型以外了,这也就是失血模型的根本问题所在。

除此保证内部一致性以外的动作方法可以不用放在领域模型内部,这些和业务场景有关的交互行为可以在服务中,也可以使用DCI将接口注入领域模型中,还可以用消息或事件实现。也就是说,用消息来实现交互,不管这种交互是由事件引起的,还是领域模型对技术架构发出的一种命令。

其实,聚合根也可以看成是一种角色,其职责是:维持聚合边界内状态的一致性(逻辑一致性)。

因此,聚合根与DCI可以结合,如下图:

在一个聚合设计中,我们可以考虑DCI,比如A实体是聚合根,也就是说,A已经固定扮演了聚合根这个角色,如果我们还希望A实现其他场景的角色职责?怎么办?

比如希望让A扮演持久化的角色,或者让A实现消息生产者的角色,这些职责虽然不是业务场景职责,毕竟A是生活在计算机世界中,也要遵守计算机领域场景的一些规则游戏。

使用Mixin/AOP实现的动态组合太多角色可能破坏A实体充当聚合根这个主要角色,在这种情况下,以DCI名义只引入一个事件发送者角色,让A实体主要实施聚合根职责,其他以外的职责全部通过以事件消息的形式委托其他类来实现。

http://www.jdon.com/45318案例为说明如下,BacklogItem假设等同与聚合根A, product相当聚合根B, A和B的依赖交互可用消息事件实现:


public class BacklogItem{
//ProductVO是另外一个聚合根实体Product的值对象
private ProductVO productVO;

@Inject
//DCI的角色注入句柄 组入(织入)一个领域事件发送者角色
private DomaineventsRole domaineventsRole;

//需要交互的方法
public void updateProduct(){
//向Product聚合根发事件消息实现交互操作
domaineventsRole.send(new ProductUpdatedEvent(productVO.getProductId));
}

//开始记录方法会改变自身内部状态,直接作为基本方法。
public void startLog(){
..
}

}

[该贴被banq于2013-04-23 08:28修改过]
[该贴被banq于2013-04-23 09:43修改过]

banq,在网上找到了一个比较典型的例子,就是处理“会议位置订单”的一个流程,流程图如下:

更多信息见这个地址:http://msdn.microsoft.com/en-us/library/jj591569.aspx

请问,按照你关于角色的思路来分析,该如何实现上图呢?

注意,上图的流程中的实现特色是有一个中心节点,就是order process manager,负责协调整个订单处理的过程。

但我知道,按照你的关于角色的理论应该是没有这个中心节点的。我多次在你的网站上提到“saga”,"processmanager"字样的概念,但你都直接无视,这次我希望你能明确思考和回答下,如果用去中心化的角色之间直接交互的设计,针对上图的流程,我们该如何定义角色,如何设计每个角色的交互行为呢?
[该贴被tangxuehua于2013-04-23 18:46修改过]

这个问题非常关键,我觉得要弄清楚2点:
1、对象为什么会拥有行为?是主动式的?还是响应式的?

2、对象如何拥有行为?

对于第一个问题,我认为“对象因为受到刺激而发生响应(行为)“。

对于第二个问题,我认为”对象拥有行为“并不是简单的”贫血对象“或者”充血对象“的问题,而是要如何组织那个”驱动对象发生行为“的因。对于目前DDD采取的”聚合根“,我并不太赞同,我认为”一致性“既然是对象之间必然的业务联系,那么完全可以通过”事件响应“去保证”一致性“,可以不必关注什么”集合根“。

banq的观点是:
1. data(aggregate)无交互行为,role有交互行为;
2. 要实现整个业务流程,如我上图贴的例子,不需要一个中央协调者(Order Process Manager),完全凭各种角色相互交互就能完成;

我的观点是:
1. 如果业务上来看就是一个流程,那就应该设计一个流程,如上面我提到的Order Process Manager来体现这个流程,用它来负责协调整个流程的交互;在这个过程中,流程中的每个节点只负责更新自己,然后中央协调者监听每个流程节点所发出来的domain event,然后中央协调者根据流程当前的状态决定后续的步骤该怎么走;这个思路就是和我上图贴的是一种做法;

flyzb的观点是:
1. 对象不会主动执行某个行为,必定是受外界刺激后才发生响应;
2. 至于flyzb说的第二点和本帖子关系不是很大,因为这是一个如何实现数据一致性的问题;

总结,我觉得我们现在要弄清楚的是,到底该用哪种方式来实现类似上图我所提到的流程?

DCI是银弹?or 还是process manager才是正确的选择?
[该贴被tangxuehua于2013-04-23 21:08修改过]

本来写了一大堆,不过越想越不对劲,期待banq能针对上面的示例进行一个具体的模型分析和设计。
[该贴被lovko于2013-04-23 23:11修改过]
[该贴被lovko于2013-04-23 23:17修改过]

我前面讲的是模型对象内部的方法行为设计,如果将这些模型对象放入一个流程这样大环境下考虑,是需要BPM流程管理器之类Process manager进行协调的。

但是在大多数不需要流程管理的场景下,多个聚合根模型之间直接通过消息事件就可以完成协调交互。

关于聚合根一致性的案例可见前面讨论的:
DDD CQRS和Event Sourcing的案例:足球比赛 :
http://www.jdon.com/44815

以比赛Match代码为例子:


public class Match {

private String id;

private Date matchDate;

private Team teams[] = new Team[2];

private boolean finished;

@Inject
public EventSourcing es;

//开始比赛方法保证比赛本身的内部一致性
public void startMatch(Date matchDate) {
//如果没有比赛时间 比赛是无法开始 这是一致性校验
if (this.matchDate != null)
System.err.print(
"the match has started");
es.started(new MatchStartedEvent(this.id, matchDate));
}

//结束比赛方法也是需要保证比赛内部逻辑一致
public void finishWithScore(Score score, Date matchDate) {
if (this.matchDate == null)
System.err.print(
"the match has not started");
//如果比赛已经结束,再次结束肯定破坏比赛状态
if (finished)
System.err.print(
"the match has finished");
es.finished(new MatchFinishedEvent(this.id, matchDate, score.getHomeGoals(), score.getAwayGoals()));
}

...
}

比赛对象的字段有:比赛有开始时间 开始状态 结束时间 结束状态。

这些字段之间有约束规则,形成逻辑一致性,才能真正形成比赛这个概念:
1. 比赛必须有开始时间和结束时间,不能为空。
2. 比赛开始后,只能结束,不能再开始。
3. 比赛结束后,一切都Over,不能再有结束或开始动作。

在Match这个聚合根内部加入这些维持自身一致性的行为,从而保证Match状态的逻辑一致性,如果这些行为放在Match外部去实现,造成Match内部字段数值混乱,没有可控性,这和直接操作数据表没有什么两样。

打个比喻,人作为一个实体对象,有维持自身生命的行为,如果没有这些行为,就不是活人了;这是其基本职责;而人在家里是爸爸,在单位是经理,可以签署文件,这些行为都是因为其角色使然,也就是他在单位这个业务场景,扮演的角色需要的职责。

如果区分了这两种职责,那么我们可能对领域模型的行为设计比较清楚了。

总结:搞软件只要掌握两个一致性即可:
1.通过DDD聚合根掌握业务逻辑上一致性,保证软件实现需求;
2.通过CAP定理掌握数据自身的复制一致性,保证软件在技术架构包括分布式系统上准确实现。

[该贴被banq于2013-04-24 07:59修改过]

先发表下看法。看了示例代码。其实总体感觉 ,本来是想引入一个新的抽象,来清晰整个设计,但是结合实际的应用和业务场景来看。其实他增加了聚合对象的关注点。因为引入了Role,这个多出来的职责分发对象的职责其实可以更透明的来处理。而不用聚合显示的来指定。因为角色是跟上下文 相关的。与行为的关系是相对比较弱的。既然是这样。应该把上下文 与聚合分开、把上下文 与行为的关联与聚合分开。通过一个消息通讯过程中对上下文的记录和介入来实现特定场景特定业务的依赖。先说个太概的思路,后续有空会补上落实到对象设置与通讯交互流程上的图例
[该贴被lovko于2013-04-24 11:39修改过]

banq,按照你的思路,那就是大部分情况下不需要process manager,只有像上面这种流程比较明显且相对比较复杂的情况,引入process manager来负责过程协调比较合适,对吗?

那如果按照这样的思路,我觉得对象职责分配的依据就很含糊了。
因为如果你不引入process manager,那么对象之间是以某个角色的身份直接相互通信,也就是说一个聚合根对象会直接发事件消息给另一个聚合根做事情;
而如果引入了process manager,那对象不需要在发事件消息通知其他的对象做事情了,每个对象如Order, Reservation这些聚合根只要告诉外界我发生了什么,他们不必去关心后续该怎么做;然后统一由process manager来订阅每个聚合根发出来的事件,然后由process manager来决定接下来该怎么做。也就是说对象之间没有直接交互,交互的职责交给process manager了。

基于上面的分析,让我对对象职责的分配的依据产生了质疑。
为什么同样是对象交互,简单点的两三个聚合根之间的通信,就可以直接交互通信,而稍微多一个点比如4个之间的相对稍微复杂点的,就需要引入processmanager。这种交互的职责分配的依据难道是和过程是否复杂有关?

所以,真正的"交互职责"分配的核心原则到底是什么呢

2013-04-24 12:38 "@tangxuehua
"的内容
而稍微多一个点比如4个之间的相对稍微复杂点的,就需要引入processmanager。这种交互的职责分配的依据难道是和过程是否复杂有关? ...

这个疑问很好,如果系统侧重流程的可变性,比如OA办公系统,经常发生流程变更,比如请假单批准流程,今天是人事部需要签署,明天可能部门经理直接签署就可以,这种流程多变的情况下,如果由专门的流程引擎负责进行切换就方便多了。

如果是普通流程比较固定的,流程管理器引入虽然没有错,毕竟增加复杂性。

2013-04-24 11:36 "@lovko
"的内容
因为角色是跟上下文 相关的。与行为的关系是相对比较弱的 ...

我再总结一下:

数据的一致性是靠行为职责来保证的,没有行为守护的数据是一盘散沙,这也是面向数据库编程的致命问题。

行为职责分基本职责和角色职责。

基本职责:维护自身逻辑一致性的那些行为。
角色职责:在一定业务场景下扮演某个角色而具有的职责行为。

基本职责依靠DDD聚合根来划边界哦实现。
角色职责通过聚合根的事件消息交互实现,因为在一个业务场景下扮演的某个角色职责行为大部分不是孤立的,需要和其他聚合根协调,如同这次地震中,政府与民间组织是两个聚合体,两者需要一个协调机制,否则在救灾这个场景下就无法尽各自角色的职责,事情就做不好。

是否将基本职责也划为角色职责还存疑问,之前是有这个意思,称为角色基本职责,还有角色交互职责。

以上只是初步想法总结,希望对大家有帮助。

[该贴被banq于2013-04-24 13:45修改过]

就以比赛match而言,其中可能涉及的角色可能包括:
组织者
赞助商
比赛者

从banq给的match所具有的的2个方法可以看出,match并不会自己主动开始或结束,而“组织者”的决定才赋予了match开始或结束的行为。如果仅仅针对一种业务场景(组织者的决定),就在match对象里添加一些方法,那么复杂场景下这种做法仍然可能带来混乱或困扰。

我一直认为DDD是解决复杂场景问题的。

2013-04-24 13:03 "@flyzb
"的内容
比赛match而言,其中可能涉及的角色可能包括 ...

角色不一定是指人参与者,而可能是物,比如Match,match这个案例中Match有两个角色职责:
1.维护自身一致性的基本职责。
2.指挥技术架构保存自身持久化的职责。

其他更复杂的角色职责需要更详细的业务场景。如果追求流程可变性,也需要引入流程管理器。那是更大的系统了。

2013-04-24 12:59 "@banq
"的内容
政府与民间组织是两个聚合体,两者需要一个协调机制,否则在救灾这个场景下就无法尽各自角色的职责,事情就做不好。 ...

针对这点,我的认识稍微有点不一样。我支持对行为进按一定的职责来区分,但我不感冒“角色职责”。在一定的场景下扮演某个角色具有的职责行为,如果显示的把行为以角色划分。局限了系统表现力。角色是一个相对来说模糊的概念,领域提供的是一个相对明确的行为能力,具体这些能力在不同的业务场景下如何去划分,如何去归类,不影响行为本身最本质的动作,我一直把聚合看成一提供一系统有关联性逻辑的行为和状态集(类似工具类),真正的业务场景的产生是由业务系统决定是【多个聚合的多种行为有机的结合后产生的】,从这个层面理解,聚合对业务场景是不了解的,所以我习惯的设计是把业务场景独立与聚合之外,由第三方构件去组织识别。banq你这个设计可以理解成把角色已经委派给了角色对象去完成,根据模型在不同的应用中,添加领域角色来完成领域聚合与当前应用的依赖组合。但从你这个代码示例中

// 在此输入java代码
es.started(**)
es.finished(**)

就这started、finished两个关键词,一定程序上就让消息在领领域聚合内部就集成了一定的上下文语义。将行为与消息关联到了一起,我倾向换成Raise,而且体这个消息处理一个怎么的业务场景中,由第三方构件去处理。 我理解的消息更多的是一种状态的描述,产生消息的聚合本身它已经拥有掌控聚合内部状态的一切信息,所以消息是给外部对象使用的。既然是外部系统关注的消息,如何去处理这个消息,如果与一个事务操作中,其它聚合发出的消息 进行上下文 关联,来产生特定的业务场景,这个应该交给专门的上下文对象作为载体。而肯体产生怎么的依赖,应该由业务系统去扩展消息总线留出的Filter切入点,因为场景总是各个聚合组合去完成一个事务才会产生。
总结:保持消息的快照特性,仅表达聚合本身的行为与状态快照,这样来保持聚合自身的单纯无关联性。而处理这种关联让业务场景后期运行时介入。通过上下文记录的消息链,上下文 状态来处理聚合之间的行为关联,而聚合和聚合之间完全不可知的,这个业务关联装配的过程由消息管道中的场景Filter来调度协调,而这个Filter由使用领域的人动实现。领域是可复用的。业务场景的复用度和可变因子都相不稳定,我会把它提到更上层的业务系统中。


顺便报告一个在chrome下的问题(其它浏览器没验证):
我长篇大论的写了一篇回复,结果因为一条“谁谁谁发表了****的回复”的提示窗口弹出变没了,有点无语了……