实战DDD(Domain-Driven Design领域驱动设计:Evans DDD)
板桥里人 http://www.jdon.com 2006/7/10(转载请保留)
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是告诉我们如何做好业务层!并以领域驱动设计思想来选择和合适的框架,本文以基于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;
/**
public int getMessageCount() { ...... |
同样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集合。
失血模型
MF(Martin Fowler)曾经提出有名的贫血模型或失血模型,让我们好生迷惑和彷徨,他认为实体模型对象中只有弱行为setter和getter方法,没有真正行为,好像缺少血液的人,不和谐了,不少高手又被忽悠了,大谈贫血模型。
其实,Eric已经认为,在DDD中,领域中一些概念不能作为模型中的对象来处理的,如果将这些功能概念强行加给实体对象和值对象,破坏模型中对象的定义,人为添加没有意义的对象。服务是描述领域概念最自然的方式。
为了在这些大师之间取得一个平衡,有人将Model的持久化操作(CRUD行为)整入到领域模型中,这是不是违背当初Dao模式初衷,Dao模式其实是桥模式和适配器模式组合(见SUN的J2EE核心模式)。
无论如何,我们的DDD项目中都是以失血模型存在着,所以,Eric呼唤:建模专家必须懂得实现,懂得软件技术,MF可能会听进去的。
领域模型驱动设计(Evans DDD)之模型提炼
板桥里人 http://www.jdon.com 2006/8/21
当Java世界提供的可选择性框架平台越来越多时,我们可能被平台架构所深深困扰,而无暇顾及软件的真正核心:业务建模,其实,业务领域建模同样是一个比平台架构更复杂,更需要学习的新的领域。
相反,在实践中,我们技术人员在经过冗长的平台架构学习和实践后,就匆忙开始项目开发,这时是什么指导他们进行软件业务实现呢?大部分可能是依赖数据库建模,甚至是复杂冗长的数据库存储过程设计,这些已经开始走向面向对象分析设计的反方向,走上了一条错误的软件开发方向,最终开发出缓慢的、经常当机的Java企业系统。
如果你没有恰当的OO设计思想,Java就会用性能惩罚你,这可能是Java世界的一个潜规则。
那么,一个正确的OOA/OOD/OOP步骤是什么呢?目前围绕模型驱动设计(MDD)的设计思想成为主流思想,MDA更是在MDD基础上提升和升华。下面让我们首先了解,如何使用领域驱动设计思想来分析设计一个软件系统。
当我们不再对一个新系统进行数据库提炼时,取而代之的时面向对象的模型提炼。我们必须大刀阔斧地对业务领域进行细分,将一个复杂的业务领域划分为多个小的子领域,同时还必须分清重点和次要部分,抓住核心领域概念,实现重点突破。
核心领域模型
精简模型,找出核心领域,将业务需求中最有价值的概念体现出来,让核心变精要,这实际就是一个使复杂问题变简单的过程,也是对我们软件设计人员真正能力的考验。
核心领域模型不是轻易能够发现,特别是他处于一个纷乱复杂的众多领域模型结构中时,核心模型通常是我们某个子领域关注的重点,例如订单模型是订单管理领域的核心;消息模型是论坛或消息领域系统的核心。
目前,分析领域有很多模式来帮助我们来提炼核心模型,例如四色原型、Martin Fowler 的分析模式等,例如MF的"分析模式"(Analysis Patterns)中的记帐模型就是不仅仅用来记录账目数值,而且可以记录和控制账目的每一次修改。而四色原型则是一种高于分析模式的一种原型基本模式,下面是本人根据四色原型提炼的核心领域模型概念。
一般情况下,在企业应用中,核心模型总是在其周围围绕一些所谓的“卫星”,这实际上也是来自四色原型的一个推论,核心模型和其“卫星”的类图如下:
根据Eric Evans在其“领域驱动设计”一书中定义,领域模型划分为实体和值对象两种,实体模型是指业务领域中具有独立属性的对象;而值对象则可能是一种Description或状态或规则。只要有实体对象,就可能存在实体的状态,状态跟踪有时成为一个业务领域使用计算机软件的首要跟踪,但是,数据库不是对象状态的唯一表达方式,只是一种存储方式(见状态对象:数据库的替代者)。
图中,实体核心对象大部分可能有一种类型,例如核心模型是产品,那么存在产品目录;核心模型是消息;就存在消息类型;核心模型是信息;总存在信息类别,我们总是使用分类方式来管理业务领域的信息,有时,类别甚至复杂到树形结构。
核心实体模型有时会有一个1:N关联的子实体,一般可能表达实体的细节,例如:核心模型是订单,那么存在订单条目这样一个细节,一个订单中可能有多个订单条目;如果核心模型是信息,那么存在该信息的多个回复或评论;这样的关联一般存在多个业务领域中。
模型界面实现
原来,我们以为分析设计阶段无需了解实现细节,分析人员只要闷头做分析UML图,而无需顾及如何具体实现,其实这是一个误区。
Eric Evans在其“领域驱动设计”一书中认为:分析人员负责从领域中收集基本概念; 设计则必须指明一组适应编程工具构造的组件,以及这些组件必须能够在目标环境中有效执行。模型驱动设计(Model-Driven Design)抛弃了分裂分析模型与设计的做法,使用单一的模型来满足这两方面的要求。因此,对于核心模型必须掌握了解其实现细节。
从另外一个方面来说,中国的客户总是从界面设计来表达他们的意图(如果中国客户能够使用Use Case等UML图来表达他们概念真是不可想象),例如客户会说,我希望有一个界面让我将订单数据输入,然后能够查询符合查询条件的订单。因此,我们的核心模型至少能够顺利地映射到界面实现,相反,这个客户有这样订单界面要求,但是你没有提供一个与之适应的核心实体模型,界面实现将变得复杂,甚至走很多弯路,诞生不少DTO垃圾对象。
以JdonFramework框架实现为例子,框架提供了围绕核心模型的新增删除修改查询(CRUD)功能以及批量功能的快速实现,尤其CRUD功能实现前提是必须提炼出核心模型,从而其界面设计流程就能通过配置立即实现,这样一步到位实现领域模型到界面的过渡,可以将我们设计核心模型和客户要求的界面需求能够做到完整的统一。
开源JdonFramework下载包中message案例实际就是上述核心模型图的一种实现项目,更复杂的项目可以认为是核心模型的重叠和反复使用(从原理上讲,核心模型是四色原型的体现,而四色原型被认为是大部分企业系统的基本组成元素,见[book][UML][Peter Coad]Java Modeling in Color with UML)。
核心模型的选择
实际项目中,会存在多个核心模型的重叠和覆盖使用,主要取决于你的领域关注重点。
例如当客户和我们说要做一个旅游网站时,我们必须充分了解需求,它的软件系统重点是哪些功能。如果当他首先说:我需要一个酒店设备的查询系统,因为他的客户对酒店设备非常关注,那么我们可能认为酒店设备是这个领域模型的核心;酒店设备。如果他又进行描述:我需要一个界面,客户在输入酒店资料时,选择多个酒店设备,那么在这样一个关注领域,核心模型实际是酒店,而酒店设备可能成为酒店的一个特征实体属性,甚至是值对象了。
以进销存系统为例子,在采购系统中,采购单是一个核心实体模型,而原材料是一种辅助实体模型;在库存系统中,入出库单是一个核心实体模型,原材料或成品代表的是一个库存物品概念模型,当需要库存报表查询输出,可以立即计算出来,或将结果缓存起来,缓存起来的结果其实是库存物品对象的状态,可以使用值对象来实现。
核心模型的精练
当核心模型被定位和确定后,相当于我们抓住领域本质,这时我们可以使用面向对象的概念对模型进行精练细化,实际就是明确对象的属性,确定模型对象的边界,通过反复重构,结合GoF等设计模式,使得我们得模型准确反映本质,从而实现模型的灵活性设计。所有这些,都是数据表驱动设计所不能实现的。那你还抱着数据库建模干什么呢?
模型驱动设计(MDD)之灵活设计
板桥里人 http://www.jdon.com 2006/8/12更新
灵活设计可以使我们随着项目开发的进行,感到速度越来越快,而不是越来越慢,甚至 停滞不前。灵活设计是对领域建模的补充,当我们从领域中抓住那些隐隐约约的线索和概念原型后,就象准备好原料;下面就是通过迭代将原料锤炼成一定具体的形状,可以俗称“打铁”,那么打铁打到什么形状算可以了呢? 也就是最终希望达到什么样的设计呢?
有些软件打着“灵活性”旗号,却出现很多多余的抽象和间接层次,从而导致了复杂性,灵活性可能导致复杂性, 但是灵活性不是导致复杂性的必然原因,如何将灵活性的发展通往简单,其中精湛技巧就需要学习和不断实践, 正确的理论指导是必不可少的,模型驱动设计(MDD)提供这样一个科学的方法论。
在Eric Evans的“领域驱动设计”一书中专门探讨了这样提供灵活设计的模式和方法,下面简要述说如下:
明显意图的接口
接口的名称必须表达明显意图,而不是模棱两可,接口虽然是抽象,但是也不能抽象到别人不知你所云, 如果其他开发人员必须查看接口的实现子类才能搞清楚你这个接口的意图,那么你的接口抽象无疑是失败的,
使用明显意图的接口可以将整个子领域切分成一个个单独模块,每个模块使用带有明显意图的接口封装起来, 这种切割方式用来调整项目的焦点和对付大型系统的复杂性。
如果仅仅有大型系统开发经验,但是没有大型系统的分割经验,更重要的是良好设计理论基础,那些大型 系统开发经验也只是如过眼烟云,不会在你的程序生涯中占据多大的重要位置。
下面我们聚焦被划分成单个模块的内部设计模式:
边界影响
在软件中,操作分为:命令和查询,命令就是能够使 系统状态发生改变的操作,如增删改等操作。这些操作都可能需要有副功能,如希望增删改完成后还要返回一些结果,这些主要功能之外的副业,也称为边界影响(side effect)。一些传统过程经验的程序员经常喜欢搞“一机多用”,喜欢将很多功能揉合在一起。
大多数操作会调用其他操作,造成任意深度的嵌套,这样形成一个树形结构的调用关系,这就容易使我们很难 预测调用一个操作会产生什么样的结果,调用一个操作变得谨慎,甚至战战兢兢,虽然Ioc或DI container使 得这种嵌套关系的管理变得容易,但是不能保证每个操作本身的设计能降低复杂性,后者就是我们现在关注的。
为什么我们调用一个操作时会变得小心,因为这个操作设计时可能不执行主要功能,还有其他副功能,这些 副功能可以认为是一种多余副作用,解决办法很简单:设计这个操作时消灭副作用;如果不能消灭,就将其 分离显式分离并单独表达出来。
所以,我们设计增删改命令和查询功能时,尽可能分离它们到不同操作中实现,不要在增删改命令执行的同时 返回任何领域数据。
如果边界影响不能通过设计避免,那么我们就直面它,在增删改等命令执行同时返回数据,当这样做的同时, 我们就使用断言assertion来约束我们这样的设计,通过断言能够易于使用单元测试Junit等工具测试。从而 保证你的命令简单有规则。
总之还是那句有些哲学意义的话:对于边界功能,首先要去除它,如果不能回避它,就承认它,但是同时会约束它。
边界影响主要的是Service接口怎么做的问题。在实际项目中,Model和Service是相互结合不断重构的。那么Model和Service粒度是如何界定的?
粒度界定
对象的粒度到底是多大?这没有一个统一的规律,哪些属性或行为属于A对象,哪些属于B对象,有时又需要将这些属性和行为分离以便能够灵活组合。我们一般很难确定类的粒度到底是怎样规模表示达到设计目的?
首先,粒度不能太大,造成重复和冗余,很多概念混在一起;当然粒度也不能太细,以至于太碎,不能完整表达一个领域概念,例如半个铀原子就不再是铀。
类需要尽量准确表达领域概念,大多数领域中都包含某种逻辑上的一致性,而很多程序员有时只注意单纯的设计原则,忽视领域逻辑,设计思考时需要两条腿走路,实现平衡,比如JdonFramework的缓存设计主要应对企业领域大量查询都是在时间上相对集中的查询(如本月度),因为领域中某种逻辑存在(时间上相对集中的查询),才使得我们决定采取某种设计方案,如果没有这个前提,任何设计方案都是可行的,这也是我们通常讨论的业务场景。
每个人对业务领域中很多概念都可能不一致,但不能否认:领域中一定在某个地方存在一种旋律,我们的模型肯定能够与领域某个部分发生共振,问题关键是:我们需要通过不断发现的过程来寻找这种共振,这过程就依赖反复的重构重整refactoring。
通过重构,使我们的设计适应我们重新理解了的领域概念和业务需求,概念轮廓(Conceptual Contour)就会浮现。
注意“概念轮廓”是指对领域中概念的主要轮廓,大概样子,需要忽视不重要的细节,设计人员必须理解领域中概念在哪些地方会发生改变?哪些地方保持相对稳定,然后使设计的模型尽量与领域中概念稳定面结合起来,这样,当业务领域中概念发生变化时,我们的模型才会随之一起翩翩起舞,发生共振,从而达到模型设计反应领域本质的目的。
所以,类的粒度是要能够反应领域的概念轮廓,将能够反应概念轮廓的那些重要元素聚合到当前正在设计的类中,这也是设计中高聚合原则的体现。
高聚合、低关联是两个设计基本原则,上面我们谈了如何做到高聚合,实际上,也是反映一种类或模块的粒度设计规模。下面谈谈低关联:
消灭依赖
复杂的依赖关系无疑提高了系统的复杂性,加剧我们大脑负载,所以,我们的模型之间关系必须精练,减少依赖,最后保留下来的依赖代表了领域概念之间某种根本的关系。有的系统中,甚至是0关联,从而得到一个个被完全孤立的单独的类(standalone Class),这才是方向(也有利于我们简化配置Hibernate这些模型持久化框架,无需考虑一对多等关联配置)。
每个依赖都是值得怀疑的,这是我们思考的前提,重构时,一个个去消灭那些象蜘蛛网一样密集的关系,斩断它们,低关联时减轻概念过载问题的基本方法,孤立类是低关联达到极致的一种标志。
减少依赖不是意味不考虑业务领域场景概念,武断实现,依赖和之前描述的边界影响一样,有时确实不能消灭,那么我们就承认它,但是会又有一套设计原则来约束它。