有必要为领域驱动设计的设计概念定义“统一语言”,通过理解的一致保证交流的畅通,确保架构和设计方案的统一性。。
当我们在讨论领域驱动设计时,不止要谈到领域驱动设计固有的设计概念,结合开发语言和开发平台的设计实践,又会有其他设计概念穿插其中,它们之间的关系并非正交的,解决的问题和思考的角度都不太一致,许多设计概念更有其历史渊源,却又在提出之后或者被滥用,后者被错用,到了最后已经失去了它本来的面目。 因此,我们首先需要揭开这些设计术语的历史迷雾,理解其本真的概念,然后再确定它的统一语言。
POJO 对象
POJO(Plain Old Java Object)的概念来自Martin Fowler、Rebecca Parsons和Josh MacKenzie在2000年一次大会的讨论。 它的本质含义是指一个常规的Java对象,不受任何框架、平台的约束和限制。 除了遵守Java语法之外,它不应该继承预先设定的类、实现预先设定的接口或者包含预先指定的注解。 如果一个模块定义的对象皆为POJO,那么除了依赖JDK之外,它不会依赖任何框架或平台。 在.NET框架中,借助这个概念,也提出了POCO(Plain Old CLR Object)的概念。
Martin Fowler等人之所以提出POJO,是因为他们看到了使用POJO封装业务逻辑的益处,而在2000年那个时代,恰恰是EJB开始流行的时代,受到EJB规范的限制,Java开发人员更愿意使用Entity Bean,而Entity Bean却是与EJB强耦合的。
一些人错误地将Entity Bean理解为仅仅支持持久化的持久化对象(Persistence Object),实际并非如此。 即使是EJB规范也认为Entity Bean可以包含复杂的业务逻辑,例如Oracle的官方网站对Entity Bean的定义就包括:
管理持久化数据
通过主键形成唯一标识
引入依赖对象执行复杂逻辑
当Entity Bean还封装了复杂的业务逻辑时,带来的危害更多。 由于定义一个Entity Bean类需要继承自javax.ejb.EntityBean,这就使得业务逻辑与EJB框架紧耦合,不利于对业务逻辑的测试、部署与运行。 这也正是Rod Johnson要提出抛开EJB进行J2EE开发的原因。 当然,Entity Bean与EJB框架紧耦合的为人诟病,主要是针对EJB 3.0之前的版本,随着Spring与Hibernate等轻量级框架出来之后,EJB也开始向轻量级方向发展,通过大量使用标注来降低EJB对Java类的侵入性。
总之,POJO对象并非只有get/set方法的贫血对象,它的主要特征不在于它究竟定义了什么样的成员,而在于它作为一个常规的Java对象,并不依赖于除语言之外的任何框架。 当然,它的目的不在于数据传输,也不在于数据持久化,它其实是一种设计模式。
Java Bean
严格讲来,Java Bean其实是一种Java开发规范。 一个Java Bean类必须同时满足以下三个条件:
类必须是具体的、公共的
具有无参构造函数
提供一致性设计模式的公共方法将内部字段暴露为成员属性,即为内部字段提供规范的get和set方法
认真解读这三个条件,你会发现它们都是为框架通过反射访问类成员而准备的前置条件,包括创建Java Bean实例和操作内部字段。 只要遵循Java Bean规范,就可以采用完全统一的一套代码实现对Java Bean的访问。 这一规范并没有提及业务方法的定义,这是因为规范无法对公开的方法做出任何一致性的限制。 这意味着框架使用Java Bean,看重的其实是该对象携带的数据,且能够减少不必要的访问代码。 例如,JSP对Java Bean的使用:
<jsp:useBean id="student" class="com.sample.javabeans.Student">
<jsp:setProperty name="student" property="firstName" value="Bill"/>
<jsp:setProperty name="student" property="lastName" value="Gates"/>
<jsp:setProperty name="student" property="age" value="20"/>
</jsp:useBean>
JSP标签中使用的Student类就是一个Java Bean。 如果没有遵循Java Bean规范定义类,JSP就可能无法实例化Student对象,无法设置firstName等字段值。
至于Session Bean、Entity Bean和Message Driven Bean则是Enterprise Java Bean的三种分类,它们都是Java Bean,但EJB对它们又有框架的约束,例如Session Bean需要继承自javax.ejb.SessionBean,Entity Bean需要继承自javax.ejb.EntityBean。
通过追本溯源,就可以发现POJO与Java Bean并没有任何关系。 一个POJO如果遵循了Java Bean的设计规范,可以成为一个Java Bean,但并不意味着POJO必须是Java Bean。 反过来,一个Java Bean如果仅仅遵循了设计规范,并没有依赖任何框架,也可以认为是一个POJO。 但是,Enterprise Java Bean一定不是一个POJO。
贫血模型
贫血模型准确地说,应该被称之为“贫血领域模型(Anemic Domain Model)”,因为这个术语其实是在领域模型这个语境中使用的。 这个术语来自Martin Fowler的创造,从贫血这个词可知,这样的一种领域模型必然是不健康的,它违背了面向对象设计的关键原则,即“数据与行为应该封装在一起”。 在领域驱动设计中,会导致贫血模型的对象是实体与值对象。 如果一个实体或值对象除了内部字段之外,就只有一系列的getter/setter方法,它就成为了贫血对象。
关于贫血领域模型的坏处,我在本书已经阐述了很多,例如它会影响对象之间的协作方式,它违背了“迪米特法则”与“信息专家模式”,它会导致“特性依恋”坏味道的产生,最后,它其实破坏了封装,导致领域服务形成一种事务脚本的实现。
与贫血领域模型相对的是富领域模型(Rich Domain Model),也就是封装了领域逻辑的领域模型。 这才是符合面向对象设计思想的领域设计建模。 在领域模型设计建模过程中,我们定义的实体与值对象都应该建立为富领域模型。 这才是真正的领域模型,也就是Martin Fowler在《企业应用架构模式》中定义的领域模型模式,作为一种领域逻辑模式(Domain Logic Pattern),它与事务脚本(Transaction Script)、表模块(Table Module)属于不同的表达领域逻辑的模式。 倘若遵循这一模式的定义,即默认为领域模型就应该是富领域模型,而贫血领域模型会导致事务脚本,不应该将这样的模型称为领域模型。
当我们在讨论领域模型时,发现更有好事者在贫血模型的基础上衍生出各种与“血”有关的各种模型,统计下来,除了Martin Fowler提出的贫血模型之外,还包括失血模型、充血模型与胀血模型。 我将这些模型戏称为“X血模型”。 我个人其实并不赞成制造出这么多的模型。 实际上,贫血模型的核心关键在于面向对象设计思想的职责分配。 如果我们能够按照合理的职责分配原则来设计领域模型,就无所谓这些X血模型了。 只要职责分配合理,有可能领域模型中的一个类确乎没有定义具有领域逻辑的行为,那也只能说明该领域概念确实不具有领域逻辑,那么这样的类也不应当称之为是贫血对象。
我还看到有的人错误的理解或者误用了“贫血模型”的定义,将只有字段和字段的getter/setter方法的类称之为“失血模型”,而将Martin Fowler提出的富领域模型称之为“贫血模型”。 这一说法绝对是“谬种流传”。 顾名思义,贫血(Anemic)这个词代表着不健康,贫血模型当然就意指不健康的模型。 如果采用这篇文章的定义,Martin Fowler所推崇的富领域模型反倒成了不健康的贫血模型(虽然该文作者未必认为贫血模型不健康),该何其无辜啊! 因此,在这些“X血模型”中,我们必须坚定果断地去掉失血模型,并让贫血模型回到本初的含义上。
在去掉了错误的失血模型后,一般认为充血模型其实就是Martin Fowler提出的富领域模型。 我总觉得“充血”这个词仍然带有不健康的隐含意义,故而不愿意使用这一模式名称,更不用说更加惊悚的“胀血模型”了。 这种胀血模型违背了单一职责原则,将与该领域概念相关的所有逻辑都放到了领域模型对象中,包括对持久化类(如传统的DAO,DDD中的Repository)的依赖以及事务、授权等横切关注点的调用。 这实际上就是让一个领域模型对象承担了聚合、领域服务以及应用服务的职责,这样的模型显然有悖于面向对象的设计原则。
有的观点认为混入了持久化能力的领域模型属于充血模型,这更进一步混淆X血模型的边界。 实际上,Martin Fowler将这种对象称之为“活动记录(Active Record)”,它属于数据源架构模式(Data Source Architectural Patterns)中的一种。 这种设计方式是领域驱动设计努力避免的,如果让每个实体都混入了持久化能力,就会丢失聚合的边界保护作用,资源库也就失去了存在的价值。
还有人混淆了领域模型与POJO的概念,认为贫血模型对象就是一个POJO,殊不知这二者根本就是两个迥然不同的维度。 POJO关注类的定义是否纯粹,领域模型关注的是对领域逻辑的表达与封装。 即使是一个只有getter/setter方法的贫血模型对象,只要它依赖了任何外部框架,例如标记了javax.persistence.Entity标注,它也不属于一个POJO。 严格说来,Dubbo服务化最佳实践给出的建议——“服务参数及返回值建议使用POJO对象,即通过setter, getter方法表示属性的对象”,对POJO的描述也是不正确的,因为Dubbo服务的参与与返回值需要支持序列化,这不符合POJO的定义。
由此可以看出,定义种类繁多的模式会让人“乱花渐欲迷人眼”,随着信息的多次传递,就会迷失它们本来的面目。 我们需要做减法,在领域驱动战术设计中,只要遵循领域驱动设计的原则定义了实体、值对象、领域服务和应用服务,就不用考虑这些模式,只需要把握一条: 避免设计出贫血领域模型!
XO
在分层架构的约束下,在职责分离的指引下,一个软件系统需要定义各种各样的对象,在分层架构中承担了不同的职责,又彼此协作,共同响应系统外部的各种请求,执行业务逻辑,并让整个软件系统能够真正地跑起来。 然而,若没有真正理解这些对象在架构中扮演的角色,承担的职责,导致误用和滥用,就有可能适得其反。 因此,有必要在领域驱动设计的方法体系下,将各式各样的对象进行一次梳理,形成一套统一语言,就不至于出现理解上的分歧,使用上的不当。 由于这些对象皆以O结尾,故而戏称为XO对象。
这些XO对象包括:
DTO
DTO (Data Transfer Object,数据传输对象) 用于在进程间传递数据,远程服务接口的输入参数与返回值都可以认为是一个DTO。我个人又根据调用者的不同,将其分为视图模型对象与消息契约对象。DTO必须支持序列化,同时它通常应该设计为一个Java Bean,即定义为公开的类,具有默认构造函数和getter/setter方法。这样就有利于一些框架通过反射来创建与组装DTO对象。DTO还应该是一个贫血对象,因为它的目的是为了传输数据,没有必要定义封装逻辑的方法。
VO
VO(View Object,视图对象)其实是DTO的一种,即我提到的视图模型对象。 本质上,它应该遵循MVC模式,为前端的视图提供数据,即MVC中的模型对象。 视图对象可能仅传输视图需要呈现的数据,但也可能为了满足前端UI的可配置,由后端传递与视图元素相关的属性值,如视图元素的位置、大小乃至颜色等样式信息。 在领域驱动设计中,有些人会将值对象(Value Object)简称为VO,要注意不要混淆这两个缩写。
BO
BO(Business Object,业务对象)这是一个非常宽泛的定义,在软件系统中,它的定义来自于分层架构的定义,即数据访问层、业务逻辑层与UI呈现层。 故而BO就是定义在业务逻辑层中封装了业务逻辑的对象。 在领域驱动设计中,可以认为就是领域对象。 为避免混淆,我建议不要在领域驱动设计中使用该概念。
DO
由于领域驱动设计将业务逻辑层分解为应用层和领域层,业务对象在领域层中就变成了DO(Domain Object,领域对象)。 不过,在领域驱动设计中,更准确的说法是领域模型对象。 通常,领域模型对象包括实体、值对象、领域服务与领域事件。 有时候,领域模型对象单指组成聚合的实体与值对象。 宽泛地讲,只要表达了现实世界的领域概念,或者封装了领域行为逻辑,都可以认为是领域模型对象。
PO
对象字段持有的数据需要被持久化到数据表中,但不要由此认为PO(Persistence Object,持久化对象)就只能定义字段以及对应的getter/setter方法,变成一个贫血对象。 前面讨论的富领域模型对象也可以成为PO,只是在持久化时,不会用到封装在领域模型对象中的方法罢了。 由于需要告知持久化框架对象与关系表之间的映射,往往需要以某种形式在PO中展现这种关系,这就导致PO变得不够纯粹,不是一个POJO。
DAO
DAO(Data Access Object,数据访问对象)作用在映射了关系表的PO之上对其进行持久化,实现对数据的访问。 由于领域驱动设计引入了聚合边界,并力求领域模型与数据模型之间的分离,且引入了资源库(Repository)来实现对聚合的生命周期管理,因此在领域驱动设计中,不再使用DAO这个概念。
经过对以上概念的历史追寻与本质分析,我们基本上理清了这些概念的含义与用途。 在归纳到领域驱动设计这个方法体系中,我们可以得出如下统一语言:
领域模型对象包含实体、值对象、领域服务与领域事件,有时候也可以单指组成聚合的实体与值对象。
领域模型必须是富领域模型
远程服务与应用服务接口的输入参数和返回值定义为DTO,根据客户端的不同,可以分为视图模型对象与消息契约对象。
领域模型对象中的实体与值对象同时也可以作为PO
只有资源库对象,没有DAO对象
往期推荐
公司前端历数后端研发接口5宗罪
DDD如何讲清楚,做出来?
以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。本号由坐馆老司机技术团队维护。