2004年著名建模专家Eric Evans发表了他最具影响力的著名书籍:Domain-Driven Design –Tackling Complexity in the Heart of Software(中文译名:领域驱动设计 2006年3月清华出版社译本,或称 Domain Driven-Design architecture [Evans DDD])。
Martin Fowler作序说;“希望本书是一本非常有影响力的书籍,....... Eric最值得我尊敬的一个方面是他敢于讨论还未取得成功的事情”,其实,时值今年2006年,DDD开发框架已经层出不穷(如RoR、RIFE、JdonFramework等),我们项目软件包结构都变成了这样:xxx.model;xxx.service,DDD思想已经遍地开花,不能再说不成功了。
DDD是告诉我们如何做好业务层!并以领域驱动设计思想来选择和合适的框架,现在很多人总是先接触框架架构,不知道DDD,结果编出的程序很难拓展,业务逻辑都写在Service中,造成模型失血,成为数据的容器(见这个案例)。这些都是伪善OO,正确的学习顺序应该是首先学习DDD,再根据DDD和业务指导如何使用框架,如Spring Seam等。
本文以基于JdonFramework开发的JiveJdon3.0说明DDD方法的实战应用。
首先必须认识到:领域建模是一种艺术的技术,不是数学的技术,它是用来解决复杂软件快速应付变化的解决之道(快速适应需求变化的软件复用)。
我们知道软件的产生过程是:分析、设计、编程、测试、部署。过去,分析领域和软件设计是分裂的,分析人员从领域中收集基本概念;而设计必须指明一组能北项目中适应编程工具构造的组件,这些组件必须能够在目标环境中有效执行,并能够正确解决应用程序出现的问题。 模型驱动设计(Model-Driven Design)抛弃了分裂分析模型与设计的做法,使用单一的模型来满足这两方面的要求。这就是领域模型。
单一的领域模型同时满足分析原型和软件设计,如果一个模型实现时不实用,重新寻找新模型。如果模型没有忠实表达领域关键概念时,也必须重新寻找新的模型。 建模和设计成为单个迭代循环。将领域模型和设计紧密联系。因此,建模专家必须懂设计,会编程。
分层架构
最初层次只分为三层:表现层、业务层和持久层;DDD其实告诉我们如何让实现业务层!
一位道友曾经请教层次的职责,对服务Service提出疑问。根据Eric的理论,业务层将细分为两个层次:应用层和领域层。它们的定义是:应用层:定义软件可以完成的工作,并且指挥具有丰富含义的领域对象来解决问题,保持精练;不包括业务规则或知识,无业务情况的状态; 领域层:负责表示业务概念、业务状态的信息和业务规则,是业务软件核心。
层次之间必须清晰分离,每个层都是内聚的,并且只依赖它的下层,为了实现各层的最大解耦,Ioc模式和Ioc容器是目前最好的选择,JdonFramework使用基于PicoContainer的Ioc容器实现了各层的松耦合;
Eric特别指出:那种将业务逻辑交由业务界面处理的快速UI方式是旁门左道。希望象C/S结构那样可视化拖拖图形就完成的软件开发是一种错误的方向,开发时快速,难于维护和扩展,虽然使用J2EE技术,其实是一种伪多层技术。可惜,有很多国人在疯狂开发这类工具,大有不撞南墙不低头之势,并且疯狂误导很多非专业人士,可悲可叹!如果对这段言论持不同意见,建议你购买"领域驱动设计"这本译书,见P53页。
领域模型种类
传统模型分为两种:实体(Entity)和值对象(Value Object),现在服务(Service)成为第三种模型元素。
实体(Entity)定义:通过一系列连续性(continuity)和标识(identity ID)来定义;个人认为它和分析领域的四色原型中的PPT原型非常类似,可以看成是PPT原型延续。
实体必须拥有自己的唯一ID,主键,如果没有一个ID标识,为每个实例加上一个具有唯一性ID,可能是内部使用。 如JiveJdon3.0中jdonframework.xml中模型增删改查CRUD配置定义:
<model key="forumId" class="com.jdon.jivejdon.model.Forum">
.....
</model>
其中,forumId是模型com.jdon.jivejdon.model.Forum的主键,唯一ID,每个模型必须有一个专家。
值对象(Value Object):如果一个对象代表了领域的某种描述性特征,且没有概念性的标识。个人认为它是四色原型中Description原型延续。如果我们只关心模型中一个元素的属性,那么把这个元素划为值对象。值对象是不可变的,不要给它任何标识,避免实体的维护性,降低设计复杂性。我们不关心值对象是哪个实例。
在JiveJdon3.0中,ForumState是一个值对象,它表示论坛当前最新帖子、论坛的主题数量和帖子数量,它的根对象是Forum,是被内聚嵌入到Forum这个实体模型中的,代码如下:
package com.jdon.jivejdon.model;
/**
* Forum State ValueObject
* this is a embeded class in Forum.
* @author <a href="mailto:
[email protected]">banq</a>
*
*/
public class ForumState {
private int threadCount = 0; //主题数量
private int messageCount = 0;//帖子数量
private ForumMessage lastPost; //最新帖子
public int getMessageCount() {
return messageCount;
}
......
}
同样ForumThreadState是也是一种值对象,根据Eric的值对象设计,ForumThreadState和ForumState是可以合并成一个对象的,值对象中没有ID等唯一标识。
Eric认为:服务Service是描述领域概念最自然的方式,是四色原型的MI原型的延续, 优秀服务3个特征:
1.与领域概念相关的操作行为、但不是实体和值对象中固有的部分。
2.接口根据领域模型中其他元素定义
3.操作是无状态的。
在JiveJdon3中,com.jdon.jivejdon.service.ForumService和Forum实体模型及其值对象ForumState共同完成领域模型,其中ForumService属于应用服务层;而后两者属于领域层;其他服务ForumMessageService、AccountService和UploadService等都是此类性质。
领域对象的生命周期Scope
Spring 1.x刚出来时确实忽悠了大家一把,因为他没有领域对象的生命周期支持,直到Spring 2.0才将如new Bean scope,当初那些疯狂捧Spring 1.x 臭脚的所谓高手是不是还是基于数据库驱动的思维,根本没有真正OO模式思维,当今天JBoss Seam、Scopes等框架开始重视对象生命周期支持后,曾经发生在Jdon社区争战硝烟已经过去,成为历史。
Eric认为:每个对象独有器生命周期,一个对象在创建以后,可能要经历各种不同的状态,并最终消亡。 对象生命周期由长短:临时对象;常驻内存;有的与其他对象存在复杂的依赖关系;状态变化时必须满足一些不变量的约束条件。 如何管理这些对象提出挑战!处理不好会偏离MDD的方向。
在生命周期中维护对象的完整性。避免模型由于管理生命周期的复杂性而陷入困境。有 三个模式来处理:聚合(Aggregate):定义清晰的所有权和边界使模型更加紧凑,避免出现盘根错节的对象关系网;工厂(Factory)和组合(Respository)。
当一个对象生命周期之始,使用工厂和组合提供了访问和控制模型对象的方法,完善了MDD。 建立聚合的模型,并且把工厂和组合加入设计中来,可以使我们系统地对模型对象进行管理。 聚合圈出一个范伟,在这个范围中,对象无论在哪个生命周期,保持不变性。
在JiveJdon3.0中,值对象ForumState是被聚合在实体模型Forum中,Forum作为ForumState的一个根,由于它们数据必须保持一致性,不变量(invariant)是指无论何时发生数据变化必须满足一致性规则,由于根控制了访问,就无法绕过它修改内部元素,例如,如果没有Forum实体对象这个根,就无法去修改对象状态ForumState,ForumState获得是通过Forum的getter方法获得的。
ForumState和Forum的分离有可以使修改论坛状态数据(当发一个新帖时,必须更新当前论坛的最新帖子为该新帖),不会影响到Forum其他元素,特别是使用事务锁定时,不必锁住整个对象,见"领域驱动设计"书籍P92。
另外,ForumThread和ForumMessage的关联关系必设定成单向的,而不是双向的,因为领域建模中,关联越简单越好。
在JiveJdon3.0中,你可能注意到有一个com.jdon.jivejdon.service.factory.ForumBuilder,所有实体模型对象的获得都是从这个工厂创建出来的,我曾经徘徊过:这个工厂类是否应该属于持久层,因为JiveJdon3.0持久层没有使用Hibernate这样O/R Mapping框架,而是直接使用SQL,但是从持久层输出的都是对象,这是必须坚持的一个设计原则(好像是MF的一个什么元数据模式) 。
但是,Eric明确告诉我们,领域模型的工厂属于应用层,页就是还是应该处于业务层的,这样好处很多,业务层设计根本无需从Hibernate等持久层框架获得,而是从自己的工厂获得。
组合(Respository)又被翻译成仓储,我认为组合合适,主要用来返回一批对象,查询组合常用来返回批量查询结果,JdonFramework两个快速开发支持:批量查询其实应该是Respository的实现,实际也是过去Master-details的一种查询实现。
以com.jdon.jivejdon.presentation.action.ThreadListAction为例子,其功能是查询论坛Forum下所有主题ForumThread,并分页显示,实现效果按这里,我们在customizeListForm方法中将根Model Forum设置进入,在threadList.jsp中,我们使用struts的标签库logic:iterator来遍历组合对象threadListForm中的ForumThread集合。
JiveJdon中聚合根和边界
失血模型
MF(Martin Fowler)曾经提出有名的贫血模型或失血模型,让我们好生迷惑和彷徨,他认为实体模型对象中只有弱行为setter和getter方法,没有真正行为,好像缺少血液的人,不和谐了,不少高手又被忽悠了,大谈贫血模型。
其实,Eric已经认为,在DDD中,领域中一些概念不能作为模型中的对象来处理的,如果将这些功能概念强行加给实体对象和值对象,破坏模型中对象的定义,人为添加没有意义的对象。服务是描述领域概念最自然的方式。
为了在这些大师之间取得一个平衡,有人将Model的持久化操作(CRUD行为)整入到领域模型中,这是不是违背当初Dao模式初衷,Dao模式其实是桥模式和适配器模式组合(见SUN的J2EE核心模式)。
无论如何,我们的DDD项目中都是以失血模型存在着,所以,Eric呼唤:建模专家必须懂得实现,懂得软件技术,MF可能会听进去的。