自从Martin Fowler的DDD(Domain Driven Develop 领域驱动开发)提出来之后,无数的人就开始非议ORM方式下的持久化实体类,抨击这种方式下的实体类是“贫血”的,缺乏丰富业务语义的。其实他们都犯了一个最基本的逻辑错误 - 偷换概念。
概念是如何被偷换的呢?请注意,领域模型(Domain Model)是一个商业建模范畴的概念,他和软件开发并无一丝一毫的关系,即使一个企业他不开发软件,他也具备他的业务模型,所有的同行业的企业他们的业务模型必定有非常大的共性和内在的规律性,由这个行业内的各个企业的业务模型再向上抽象出来整个行业的业务模型,这个东西即“领域模型”。一个掌握了行业领域模型的软件公司,根本不需要再给人家开发项目了,根本不需要靠软件开发养活自己了,你光给这个行业的企业提供业务咨询已经赚得非常丰厚的利润了。以我现在兼职所在的公司来说,就是这样一家软件公司,在行业内积累了足够的领域模型,成立了一个专门的咨询部门,这个部门下面都是咨询师,他们是不管软件开发的,也不懂软件开发,他们就专门教这个行业的客户,教他们怎么去做自己的业务,他们比客户还精通客户的业务,光是业务咨询已经可以为公司带来很多的收入。
而软件开发呢?一个并没有行业经验积累的软件公司,它开发的软件,基本上完全是需求驱动,而不是领域模型驱动。只有具备了领域模型积累的公司才有资格去谈领域模型驱动软件开发。在由领域模型往某种编程语言如Java上来实现的时候,绝对不会是1:1的对应关系,即使是粗颗粒度的EJB2模型都做不到,更不要说更加强调细颗粒度的POJO模型呢?用面向对象的语言如Java来编写一个领域模型,如果是用EJB2模型,你需要使用最少两个以上的EJB,即一个 Session Bean,处理面向流程的控制逻辑,一个Entity Bean,处理面向持久化的实体逻辑(持久化操作附着在Entity Bean的Home接口上)。如果是更加复杂的领域模型,那么你需要更多的EJB,也许是一个领域模型需要多个Entity Bean和多个Session Bean。现在我们使用基于POJO模型的实现,那么粗颗粒度的EJB还要继续细分:一个Entity Bean要剥离出来至少三个以上的POJO,即一个或者多个实体类,一个或者多个DAO接口类,一个或者多个DAO接口实现类;一个Session Bean要切分为多个业务Bean。
由此我们终于看出来概念是怎样被偷换的了,一个商业概念的抽象领域模型被一个Java持久化实体类替代了。但是我们应该看到,Martin批评的贫血的领域模型并不是Hibernate实体类,Martin指的贫血的领域模型实际上是缺乏丰富业务逻辑概念的领域抽象模型,这和Hibernate实体类完全是风牛马不相及的东西。而Hibernate实体类只是具体编码过程中,为了实现一个领域模型而编写的一组基于POJO的对象中的,完成领域模型某个特征的类。而这个领域模型完整的特征并不应该,也不可能由一个非常粗颗粒度的单类完成,而是由一组互相协作的类完成:即Hibernate的实体类保持领域模型的状态;DAO接口实现类完成领域模型的持久化操作;Spring Bean类完成领域模型的逻辑控制功能。
POJO指的就是非EJB那种重量级,高侵入性的组件模型,关于POJO的定义,你同样可以在Martin Fowler的bliki上面找到。
Spring的Bean是不是POJO? 是的!
Hibernate的entity是不是POJO?是的!
DAO接口是不是POJO?是的!
EJB是不是POJO? 不是的!
我没有看过Martin的DDD,我按照自己的理解, POJO domain models指的就是轻量级的领域模型。何为轻量级? 把领域模型的各个特征,各个属性,各个逻辑都塞到一个class里面叫做轻量级吗?
我认为,Martin批评的贫血的领域模型是指只关注了领域模型持久化特征方面,而忽略了领域模型其他特征方面的模型,这样的模型是贫血的。因为这种模型只关注了模型在技术层面的外在表现,也就是说只关注了数据的存取操作,而忽视了模型蕴含的业务核心价值。
举例来说,我们编一个银行软件,如果你只关注了账户的增删改查,这叫做贫血!而实际上你应该关注的是账户的业务特征,而不是数据特征,你应该关注的是账号开立的业务,账户注销的业务,账号过户的业务等等,这才是领域模型。这种领域模型在一个单纯的技术实现层面来说,对于最简单的业务,你可能只是Account类的增删改查,但是对于复杂的业务来说,他就不单但是一个类,一个表的简单操作了,例如开立账户,你要收手续费,以及考察个人财务状况,那么此时你需要的就是一组协作的类。
Martin提到领域模型,意在强调我们应该关注软件的业务,关注行业知识的内在规律,并且把这种规律建模为领域模型,批评拿到一个软件,脑子里面光想到数据库增删改查的人。这和我们的Hibernate持久化类毫无关系!
我的看法是:一个抽象的领域模型具备多方面的特征,你需要用一组互相协作的类来完成它,每一个或者一组类承担这个领域模型的某个特征。例如某个领域模型,例如上面的账户,你需要一组Hibernate持久化类:包括Account类,User类,Finance类,一组SpringBean类,AccountManager,FinanceManager,一组DAO接口和实现类。由这些POJO的类互相协作来共同完成这个领域模型。如果你仅仅关注Account的增删改查,那就贫血了,而如果你关注了账户的业务规则,并且考虑一组互相协作的类去完成它,就不是贫血的。
从技术手段来上说,对于Spring/Hibernate架构,Martin的Rich domin model变得可行了,那么让我们看看究竟有哪些领域模型,以及他们的优缺点:
一、失血模型
失血模型请看
http://forum.javaeye.com/viewtopic.php?t=11712
中列举的第一种模型,简单来说,就是domain object只有属性的getter/setter方法,没有任何业务逻辑。
二、贫血模型
贫血模型请看
http://forum.javaeye.com/viewtopic.php?t=11712
中列举的第二种模型,简单来说,就是domain ojbect包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。
Service(业务逻辑,事务封装) --> DAO ---> domain object
这种模型的优点:
1、各层单向依赖,结构清楚,易于实现和维护
2、设计简单易行,底层模型非常稳定
这种模型的缺点:
1、domain object的部分比较紧密依赖的持久化domain logic被分离到Service层,显得不够OO
2、Service层过于厚重
三、充血模型
充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在domain object里面(包括持久化逻辑),而Service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。
Service(事务封装) ---> domain object <---> DAO
这种模型的优点:
1、更加符合OO的原则
2、Service层很薄,只充当Facade的角色,不和DAO打交道。
这种模型的缺点:
1、DAO和domain object形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。
2、如何划分Service层逻辑和domain层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序。
3、考虑到Service层的事务封装特性,Service层必须对所有的domain object的逻辑提供相应的事务封装方法,其结果就是Service完全重定义一遍所有的domain logic,非常烦琐,而且Service的事务化封装其意义就等于把OO的domain logic转换为过程的Service TransactionScript。该充血模型辛辛苦苦在domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了。
四、胀血模型
基于充血模型的第三个缺点,有同学提出,干脆取消Service层,只剩下domain object和DAO两层,在domain object的domain logic上面封装事务。
domain object(事务封装,业务逻辑) <---> DAO
似乎ruby on rails就是这种模型,他甚至把domain object和DAO都合并了。
该模型优点:
1、简化了分层
2、也算符合OO
该模型缺点:
1、很多不是domain logic的service逻辑也被强行放入domain object ,引起了domain ojbect模型的不稳定
2、domain object暴露给web层过多的信息,可能引起意想不到的副作用。
在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都已经是可行的了。但是我个人仍然主张使用贫血模型。其理由:
1、参考充血模型第三个缺点,由于暴露给web层程序拿到的还是Service Transaction Script,对于web层程序员来说,底层OO意义丧失了。
2、参考充血模型第三个缺点,为了事务封装,Service层要给每个domain logic提供一个过程化封装,这对于编程来说,做了多余的工作,非常烦琐。
3、domain object和DAO的双向依赖在做大项目中,考虑到团队成员的水平差异,很容易引入不可预知的潜在bug。
4、如何划分domain logic和service logic的标准是不确定的,往往要根据个人经验,有些人就是觉得某个业务他更加贴近domain,也有人认为这个业务是贴近service的。由于划分标准的不确定性,带来的后果就是实际项目中会产生很多这样的争议和纠纷,不同的人会有不同的划分方法,最后就会造成整个项目的逻辑分层混乱。这不像贫血模型中我提出的按照是否依赖持久化进行划分,这种标准是非常确定的,不会引起争议,因此团队开发中,不会产生此类问题。
5、贫血模型的domain object确实不够rich,但是我们是做项目,不是做研究,好用就行了,管它是不是那么纯的OO呢?其实我不同意firebody认为的贫血模型在设计模型和实现代码中有很大跨越的说法。一个设计模型到实现的时候,你直接得到两个类:一个实体类,一个控制类就行了,没有什么跨越。