为什么在Java Web开发中不使用充血模型 事件驱动

2131人阅读
【设计模式&重构&UML建模】(63)
Martin Fowler很早以前就写过一篇文章,题目叫”贫血模型”。文章里面批判贫血的领域模型是不够优雅、不够OO的,提倡使用充血的领域模型。在Java世界里这是一直争论的话题。到底什么是贫血什么是充血呢?
贫血模型:是指领域对象里只有get和set方法,或者包含少量的CRUD方法,所有的业务逻辑都不包含在内而是放在Business Logic层。
优点是系统的层次结构清楚,各层之间单向依赖,Client-&(Business Facade)-&Business Logic-&Data Access(ADO.NET)。当然Business Logic是依赖Domain Object的。似乎现在流行的架构就是这样,当然层次还可以细分。
该模型的缺点是不够面向对象,领域对象只是作为保存状态或者传递状态使用,所以就说只有数据没有行为的对象不是真正的对象。在Business Logic里面处理所有的业务逻辑,在POEAA(企业应用架构模式)一书中被称为Transaction Script模式。
充血模型: 层次结构和上面的差不多,不过大多业务逻辑和持久化放在Domain Object里面,Business Logic只是简单封装部分业务逻辑以及控制事务、权限等,这样层次结构就变成Client-&(Business Facade)-&Business Logic-&Domain Object-&Data Access。
优点是面向对象,Business Logic符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑太过沉重。
缺 点是如何划分业务逻辑,什么样的逻辑应该放在Domain Object中,什么样的业务逻辑应该放在Business Logic中,这是很含糊的。即使划分好了业务逻辑,由于分散在Business Logic和Domain Object层中,不能更好的分模块开发。熟悉业务逻辑的开发人员需要渗透到Domain Logic中去,而在Domian Logic又包含了持久化,对于开发者来说这十分混乱。
其次,因为Business Logic要控制事务并且为上层提供一个统一的服务调用入口点,它就必须把在Domain Logic里实现的业务逻辑全部重新包装一遍,完全属于重复劳动。
如果技术能够支持充血模型,那当然是最完美的解决方案。不过现在 的.NET框架并没有ORM工具(不算上开源的NHibernate,Castle之类),没有ORM就没有透明的持久化支持,在Domain Object层会对Data Access层构成依赖,如果脱离了Data Access层,Domain Object的业务逻辑就无法进行单元测试,这也是很致命的。如果有像Spring的动态注入和Hibernate的透明持久化支持,那么充血模型还是能 够实现的。
自从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的增删改查,那就贫血了,而如果你关注了账户的业务规则,并且考虑一组互相协作的类去完成它,就不是贫血的。
○ 种一棵树最好的时间是十年前,其次是现在
○ 坚持输出,坚持书写,才可以持续成长
○ 所有美好事物的成长都是缓慢的
○ 既往不恋,未来不迎,当下不杂
○ 业精于勤,荒于嬉,行成于思,毁于随
○将军赶路 不追小兔
○不要拘泥于语言,同样也不要拘泥于行业,眼光放远一点
○ 如果某件事你做的不够好,不必介怀,因为以后的每一次每一天你都会做得越来越好
○ 此心不于事上磨,更于何处磨此心
○ 保持热情,保持求知欲
○ 千里之行,始于足下
○ 最怕你一生碌碌无为,还安慰自己平凡可贵。
○ 对于任何事,要保持自觉积极主动探索尝试。但是如果自己不积极认真地生活,不管得到什么样的回答都没有用。——解忧杂货店
访问:689713次
积分:7078
排名:第3834名
原创:295篇
转载:52篇
评论:121条
专业技能:
android, linux, Java, C/C++, Python, SQL, servlet&jsp, html
相关项目经验:
桌面应用,Android,Java web项目,Python web项目
实践输出平台:
(2)(8)(1)(9)(1)(2)(4)(6)(6)(62)(48)(54)(21)(13)(1)(37)(4)(2)(32)(15)(1)(2)(10)(6)什么是贫血模型,失血模型,充血模型 - ITeye问答
&&&& 小弟分不清楚这几个模型了,麻烦给个答案或者给个好点的文章。先谢谢啦。最好有对照的代码
采纳的答案
这里有一篇Robbin总结的帖子,你可以看看,希望对你的问题有帮助。
http://www.iteye.com/topic/11712
已解决问题
未解决问题赞助商链接
在我们应用OO进行分析设计的时候,又提出了贫血和充血的概念.并产生了很大的争论.结合DDD,在这里,我也谈谈我的想法。
1.贫血模式说白了就是把对象看成是数据的载体.因为它不存在实际的操作动作,只是各种数据的集合.从这个角度,尽管我们设计出来了对象.实质上我们只是在过程式开发模式(没有用OO语言的时候)外面包了"外套"--对象.这个对象好象是个容器,容纳了我们业务上需要处理的数据,仅此而已.所以不难想象:当我们在开发中,从需求分析到表结构确认,然后利用eclipse生成的POJO,实际上就是那些数据的载体.只是用对象给伪装了一下而已,看似OO,实则过程.
这样有一定的弊端:违反了OO的设计.对象讲究的是数据和行为的统一体,而不是分离来看.而贫血模型分离了数据和行为,领域对象作为了数据的载体,而服务对象作为了行为的集.这样我们拿面向对象的语言来做分析设计的时候就完全的抛弃了对象分析的方式.而以数据为中心进行设计.包括说的从数据库反向生成实体的方式,都是一个设计缺失的表现.
再则,我们利用spring ,hibernate进行开发的时候,产生的POJO是依托于数据库结构的.试想,这样的方式从hibernate的annotation考虑还有何意义,所谓的对象细粒度,数据库表粗粒度又如何体现?实际annotation提供的功能是我们在利用对象分析方式分析出模型后,在持久化的时候遇到的问题,hibernate给我们解决的.而不是反过来思考问题.这也恰恰曲解了hibernate的价值所在.
可能有人会说:"等依据数据库表结构生成了POJO之后,再根据POJO的情况,进行细粒度的划分,这样就可以利用annotation了".如果这样做的话,岂不是饶了个大圈又回来了,说句不好听的话叫"脱裤子放屁"啊.而我们现在的很多开发的方式,都是产生POJO后,就结束了,还没有饶圈.但是尽管不饶,方式也是错误的.如果"脱裤子放屁",那就错上加错了.这个就是典型的贫血开发.这也就说明了为什么我们会拿着OO的语言来做非OO的事情,这就是其中的原因之一.
还有一点.采用贫血开发.真正的服务,也就是我们常说的service,包含了对这些POJO的管理(CRUD),在服务里面大量充斥着管理方法.越来越庞大,随着项目的增加,越来越多的方法充斥着一个对象里面,难于管理.不光这样,实际在service里面,不光包含了我们针对一个软件应用领域的行业的规范,还包括了我们架设软件环境的基础设施的规范(事务,持久化等等),所以在一个方法里面充斥着本来属于不同层次的东西,这样的方式最终会导致混乱,难于维护是再所难免.可这恰恰是目前我们采用SSH开发软件的常规做法.
2.充血相对贫血多了点生机,有了动作了,而不是孤立的数据了.这里也和对象的概念想符合了,对象就应该是数据和行为的结合体.所以大家都会说,既然要OO的开发,就要这样去做,让数据和他们的相关行为紧密联系,而不应该分离.当然,这样的纯OO的开发思想是很不错,但是也存在一定的问题.我们的pojo如果真的是包含了数据和行为的话,必然会考虑持久化的问题.那么每一个行为都需要以事务脚本结束,也就是都必须以事务提交回滚作为终止.那么在我们持久化的时候,这些约束性的条件我们需要放置在哪里,放置在POJO里面?
如果这样做了,那么实际又是上面说的,虽然是富饶了,但是仍然将基础设施的内容与软件所属行业的规则混淆了.也会存在问题.所以我们就需要将这个层次提取出来,单独设立一层,也就是将基础设施的内容层,与业务领域的内容层分离.这样就变成了两个层次,也就是DDD中说的服务的概念,是应用层和领域层的总和.应用层只是程序级别的内容,也就是我们所说的基础设施(框架)的内容.而领域层才是业务领域里的内容的体现.领域需要依托的数据,就来自POJO(分析的实体和值对象的结果)这样的话就达到了分离的效果.但是POJO是富饶的,POJO的行为动作是领域层和应用层的总合,叫做服务,既然两者已经分离出去了,那么这个行为呆在pojo里没什么意思,索性不要了.讲到这里,实际我们不难发现DDD也是这样做的.这或许也是作者的出发点之一吧.
但是有人就说了这样的话,pojo又变穷了,没血了,说来说去,这不还是贫血吗?为什么DDD里说的领域模型是充血的而不是贫血的呢?其实这里有一点恰恰是大家忽略的问题.也就是我们思考角度的问题.
在我们没有DDD的时候,大家讨论的贫血模型里的DOMAIN OBJECT,实际上是从程序的角度来阐述的对象,而不是真正意义上的对象.他仅仅就是数据的载体而已.但是很多人都会认这样的对象是真正的对象.如果这样的数据载体也叫对象的话,那么OO的概念好象就体现不出来了.也就无所谓OO设计了.况且,我们那时候说的这个贫血对象实际有两个作用,一个用于前台页面的展示,另一个用于持久化操作.
那么DDD中说的要把逻辑放在领域对象里,看似好象和贫血模型冲突.实际上这里说的业务逻辑是领域上的逻辑,也就是业务上的验证规则,计算规则等等,并不是在贫血对象中增加一些方法当作行为这么简单的事情.领域在DDD中包括了,实体,值对象和服务.并不能单独一个实体就是一个领域对象,否则就会认为实体是贫血的,没行为可言.实际这个领域的范围更广了,是几个对象相互作用的结果.构成了领域模型,也就是实体,值对象和服务的结合体,是一个真正完整的充血模型.问题又来了:那这么做确实是充血了,符合了OO,但是OO中说的是一个对象有属性和方法,但是这里却要几个类集合起来构成一个领域模型,看似很矛盾.其实正如BANQ说的,分析问题的层次不一样导致了这种茫然,DDD是侧重从分析的角度思考问题,用这种分析的结果去知道你对象的设计.所以针对贫血和充血是从分析的角度考虑.是比我们程序代码层次的对象更高一个层次的设计.所以不在一个层次的东西,没有任何的可比性.也就是出发点的问题.我们常说的POJO就是一个结果,是分析后设计出来的结果.而贫血和充血是从你拿到需求的时候分析产生的.所以有的人说这个POJO是贫血的或者是充血的,这种说法所站的角度和DDD中贫血充血站的角度是不一样的.正如banq大哥说的,在分析的时候坚持完整的领域模型就可以了,分析和设计不一定都百分之百对应.
我们在设计的时候必然会考虑应用技术的环境.分析的层次不同,考虑问题的出发点就不同了.以上也是我个人的一点想法,有不正确的地方请指正. [该贴被lovejdon于 14:29修改过][该贴被lovejdon于 14:29修改过]
是的,失血与否其实和需求模型有非常大关系。比如DDD中模型核心是一群模型对象有一个根实体,根实体一般肯定不是失血模型,因为需要存在一些行为和方法来维持其群组中元素的一致性。就象一个组织的头要维持一个组织存在,必然在组织内部实施一些行为,保持公平等等。而群组中的元素实体,或值对象,有可能就是失血模型。所以,当初以失血和充血武断地从屁股结果上去判断,是没有益处的,关键是要依据DDD进行实际判断。不过,实战事情远非没有这么简单,见我的另外一篇文章,使用Domain Event避免贫血模型:[该贴被banq于 16:01修改过]
赞助商链接
赞助商链接
最佳分辨率
OpenSource
Code & 2002-20还没有账号
用户名或密码错误
阿里盒马领域驱动设计实践
设计是把双刃剑,没有最好的,也没有更好的,而是条条大路到杭州。同时不设计和过度设计都是有问题的,恰到好处的设计才是我们追求的极致。
DDD(Domain-Driven Design,领域驱动设计)只是一个流派,谈不上压倒性优势,更不是完美无缺。 我更想跟大家分享的是我们是否关注设计本身,不管什么流派的设计,有设计就是好的。
从我看到的代码上来讲,阿里集团内部大部分代码都不属于 DDD 类型,有设计的也不多,更多的像“面条代码”,从端上一条线杀到数据库完成一个操作,仅有的一些设计集中在数据库上。我们依靠强大的测试保证了软件的外部质量(向苦逼的测试们致敬),而内部质量在紧张的项目周期中屡屡得不到重视,陷入日复一日的技术负债中。
一直想写点什么唤起大家的设计意识,但不知道写点什么合适。去年转到盒马,有了更多的机会写代码,可以从无到有去构建一个系统。盒马跟集团大多数业务不同,盒马的业务更面向 B 端,从供应到配送链条,整体性很强, 关系复杂,不整理清楚,谁也搞不明白发生什么了 。所以这里 设计很重要 ,不设计的代码今天不死也是拖到明天去死,不管我们在盒马待多久,不能给未来的兄弟挖坑啊。在我负责的模块里,我们 完整地应用了 DDD 的方式去完成整个系统 ,其中有我们自己的思考和改变,在这里我想给大家分享一下,他山之石可以攻玉,大家可以借鉴。
领域模型探讨
1. 领域模型设计:基于数据库 vs 基于对象
设计上我们通常从两种维度入手:
Data Modeling : 通过数据抽象系统关系,也就是数据库设计
Object Modeling : 通过面向对象方式抽象系统关系,也就是面向对象设计大部分架构师都是从 Data Modeling 开始设计软件系统,少部分人通过 Object Modeling 方式开始设计软件系统。这两种建模方式并不互相冲突,都很重要,但从哪个方向开始设计,对系统最终形态有很大的区别。
Data Model
领域模型(在这里叫数据模型)对所有软件从业者来讲都不是一个陌生的名词,一个软件产品的内在质量好坏可能被领域模型清晰与否所决定,好的领域模型可以让产品结构清楚、修改更方便、演进成本更低。
在一个开发团队里,架构师很重要,他决定了软件结构,这个结构决定了软件未来的可读性、可扩展性和可演进性。通常来说架构师设计领域模型,开发人员基于这个领域模型进行开发。“领域模型”是个潮流名词,如果拉回到 10 几年前,这个模型我们叫“数据字典”,说白了, 领域模型就是数据库设计 。
架构师们在需求讨论的过程中不停地演进更新这个数据字典,有些设计师会把这些字典写成 SQL 语句,这些语句形成了产品 / 项目数据库的发育史,就像人类胚胎发育:一个细胞(一个表),多个细胞(多个表),长出尾巴(设计有问题),又把尾巴缩掉(更新设计),最后哇哇落地(上线)。
传统项目中,架构师交给开发的一般是一本厚厚的概要设计文档,里面除了密密麻麻的文字就是分好了域的数据库表设计。言下之意: 数据库设计是根本,一切开发围绕着这本数据字典展开 ,形成类似于下边的架构图:
在 service 层通过我们非常喜欢的 manager 去 manage 大部分的逻辑,POJO(后文失血模型会讲到)作为数据在 manager 手(上帝之手)里不停地变换和组合,service 层在这里是一个巨大的加工工厂(很重的一层),围绕着数据库这份 DNA,完成业务逻辑。
举个不恰当的例子:假如有父亲和儿子这两个表,生成的 POJO 应该是:
public class Father{…}
public class Son{
private String fatherId;//son 表里有 fatherId 作为 Father 表 id 外键
public String getFatherId(){
return fatherId;
这时候儿子犯了点什么错,老爸非常不爽地扇了儿子一个耳光,老爸手疼,儿子脸疼。Manager 通常这么做:
public class SomeManager{
public void fatherSlapSon(Father father, Son son){
// 如果逻辑上说不通,大家忍忍
father.setPainOnHand();
son.setPainOnFace();// 假设 painOnHand, painOnFace 都是数据库字段
这里,manager 充当了上帝的角色,扇个耳光都得他老人家帮忙。
Object Model
2004 年,Eric Evans 发表了《Domain-Driven Design –Tackling Complexity in the Heart of Software》(领域驱动设计),简称 Evans DDD,先在这里给大家推荐这本书,书里对领域驱动做了开创性的理论阐述。
在聊到 DDD 的时候,我经常会做一个假设:假设你的机器内存无限大,永远不宕机,在这个前提下,我们是 不需要持久化数据的 ,也就是我们可以不需要数据库,那么你将会怎么设计你的软件?这就是我们说的 Persistence Ignorance:持久化无关设计。
没了数据库,领域模型就要基于程序本身来设计了,热爱设计模式的同学们可以在这里大显身手。在面向过程、面向函数、面向对象的编程语言中, 面向对象无疑是领域建模最佳方式 。
类与表有点像,但不少人认为表和类就是对应的,行 row 和对象 object 就是对应的,我个人强烈 不认同 这种等同关系,这种认知直接导致了软件设计变得没有意义。
类和表有以下几个显著区别,这些区别对领域建模的表达丰富度有显著的差别,有了封装、继承和多态,我们对领域模型的表达要生动得多,对 SOLID 原则的遵守也会严谨很多:
引用 :关系数据库表表示多对多的关系是用第三张表来实现,这个领域模型表示不具象化, 业务同学看不懂。
封装 :类可以设计方法,数据并不能完整地表达领域模型,数据表可以知道一个人的三维,但并不知道“一个人是可以跑的”。
继承、多态 :类可以多态,数据上无法识别人与猪除了三维数据还有行为的区别,数据表不知道“一个人跑起来和一头猪跑起来是不一样的”。
再看看老子生气扇儿子的例子:
public class Father{
// 教训儿子是自己的事情,并不需要别人帮忙,上帝也不行
public void slapSon(Son son){
this.setPainOnHand();
son.setPainOnFace();
根据这个思路,慢慢地,我们在面向对象的世界里设计了栩栩如生的领域模型,service 层就是基于这些模型做的业务操作(它变薄了,很多动作交给了 domain objects 去处理):领域模型并不完成业务,每个 domain object 都是完成属于自己应有的行为(single responsibility),就如同人跑这个动作,person.run 是一个与业务无关的行为,但这个时候 manager 或者 service 在调用 some person.run 的时候可以完成 100 米比赛这个业务,也可以完成跑去送外卖这个业务。这样的话形成了类似于下边的架构图:
我们回到刚才的假设,现在把假设去掉,没有谁的机器是内存无限大,永远不宕机的,那么我们需要数据库,但数据库的职责不再承载领域模型这个沉重的包袱了,数据库回归 persistence 的本质,完成以下两个事情:
存 :将对象数据持久化到存储介质中。
取 :高效地把数据查询返回到内存中。
由于不再承载领域建模这个特性,数据库的设计可以变得天马行空,任何可以加速存储和搜索的手段都可以用上,我们可以用 column 数据库,可以用 document 数据库,可以设计非常精巧的中间表去完成大数据的查询。总之数据库设计要做的事情就是尽可能高效存取,而不是完美表达领域模型(此言论有点反动,大家看看就好),这样我们再看看架构图:
这里我想跟大家强调的是:
领域模型是用于领域操作的,当然也可以用于查询(read),不过这个查询是有代价的。在这个前提下,一个 aggregate 可能内含了若干数据,这些数据除了类似于 getById 这种方式,不适用多样化查询(query),领域驱动设计也不是为多样化查询设计的。
查询是基于数据库的,所有的复杂变态查询其实都应该绕过 Domain 层,直接与数据库打交道。
再精简一下:领域操作 -&objects,数据查询 -&table rows
2. 领域模型:失血、贫血、充血
失血、贫血、充血和胀血模型应该是老马提出的(此老马非马老师,是 Martin Fowler),讲述的是基于领域模型的丰满程度下如何定义一个模型,有点像:瘦、中等、健壮和胖。胀血(胖)模型太胖,在这里我们不做讨论。
失血模型:基于数据库的领域设计方式其实就是典型的失血模型,以 Java 为例,POJO 只有简单的基于 field 的 setter、getter 方法,POJO 之间的关系隐藏在对象的某些 ID 里,由外面的 manager 解释,比如 son.fatherId,Son 并不知道他跟 Father 有关系,但 manager 会通过 son.fatherId 得到一个 Father。
贫血模型:儿子不知道自己的父亲是谁是不对的,不能每次都通过中间机构(Manager)验 DNA(son.fatherId) 来找爸爸,领域模型可以更丰富一点,给 son 这个类修改一下:
public class Son{
public Father getFather(){return this.}
Son 这个类变得丰富起来了,但还有一个小小的不方便,就是通过 Father 无法获得 Son,爸爸怎么可以不知道儿子是谁?这样我们再给 Father 添加这个属性:
public class Father{
private Son getSon(){return this.}
现在看着两个类就丰满多了,这也就是我们要说的贫血模型,在这个模型下家庭还算完美,父子相认。然而仔细研究这两个类我们会发现一点问题:通常一个 object 是通过一个 repository(数据库查询),或者 factory(内存新建)得到的:
Son someSon = sonRepo.getById(12345);
这个方法可以将一个 son object 从数据库里取出来,为了构建完整的 son 对象,sonRepo 里需要一个 fatherRepo 来构建一个 father 去赋值 son.father。而 fatherRepo 在构建一个完整 father 的时候又需要 sonRepo 去构建一个 son 来赋值 father.son。这形成了一个无向有环圈,这个循环调用问题是可以解决的,但为了解决这个问题,领域模型会变得有些恶心和将就。有向无环才是我们的设计目标,为了防止这个循环调用,我们是否可以在 Father 和 Son 这两个类里省略掉一个引用?修改一下 Father 这个类:
public class Father{
//private S 删除这个引用
private SonRepository sonR// 添加一个 Son 的 repo
private getSon(){return sonRepo.getByFatherId(this.id);}
这样在构造 Father 的时候就不会再构造一个 Son 了,但代价是我们在 Father 这个类里引入了一个 SonRepository,也就是我们在一个 domain 对象里引用了一个持久化操作,这就是我们说的充血模型。
充血模型:充血模型的存在让 domain object 失去了血统的纯正性,他不再是一个纯的内存对象,这个对象里埋藏了一个对数据库的操作,这对测试是不友好的,我们不应该在做快速单元测试的时候连接数据库,这个问题我们稍后来讲。为保证模型的完整性,充血模型在有些情况下是必然存在的,比如在一个盒马门店里可以售卖好几千个商品,每个商品有好几百个属性。如果我在构建一个店的时候把所有商品都拿出来,这个效率就太差了:
public class Shop{
//private List&Product& 这个商品列表在构建时太大了
private ProductRepository productR
public List&Product& getProducts(){
//return this.
return productRepo.getShopProducts(this.id);
3. 领域模型:依赖注入
简单说一说依赖注入:
依赖注入在 runtime 是一个 singleton 对象,只有在 spring 扫描范围内的对象(@Component)才能通过 annotation(@Autowired)用上依赖注入,通过 new 出来的对象是无法通过 annotation 得到注入的。
个人推荐构造器依赖注入,这种情况下测试友好,对象构造完整性好,显式地告诉你必须 mock/stub 哪个对象。
说完依赖注入我们再看刚才的充血模型:
public class Father{
private SonRepository sonR
private Son getSon(){return sonRepo.getByFatherId(this.id);}
public Father(SonRepository sonRepo){this.sonRepo = sonR}
新建一个 Father 的时候需要赋值一个 SonRepository,这显然在写代码的时候是非常让人恼火的,那么我们是否可以通过依赖注入的方式把 SonRepository 注入进去呢?Father 在这里不可能是一个 singleton 对象,它可能在两个场景下被 new 出来:新建、查询,从 Father 的构造过程,SonRepository 是无法注入的。这时工厂模式就显示出其意义了(很多人认为工厂模式就是一个摆设):
@Component
public class FatherFactory{
private SonRepository sonR
@Autowired
public FatherFactory(SonRepository sonRepo){}
public Father createFather(){
return new Father(sonRepo);
由于 FatheFactory 是系统生成的 singleton 对象,SonRepository 自然可以注入到 Factory 里,newFather 方法隐藏了这个注入的 sonRepo,这样 new 一个 Father 对象就变干净了。
4. 领域模型:测试友好
失血模型和贫血模型是天然测试友好的(其实失血模型也没啥好测试的),因为他们都是纯内存对象。但实际应用中充血模型是存在的,要不就是把 domain 对象拆散,变得稍微不那么优雅(当然可以,贫血和充血的战争从来就没有断过)。那么在充血模型下,对象里带上了 persisitence 特性,这就对数据库有了依赖,mock/stub 掉这些依赖是高效单元化测试的基本要求,我们再看 Father 这个例子:
public class Father{
private SonRepository sonR//=new SonRepository() 这里不能构造
private getSon(){return sonRepo.getByFatherId(this.id);}
// 放到构造函数里
public Father(SonRepository sonRepo){this.sonRepo = sonR}
把 SonRepository 放到构造函数的意义就是为了测试的友好性,通过 mock/stub 这个 Repository,单元测试就可以顺利完成。
5. 领域模型:盒马模式下 repository 的实现方式
按照 object domain 的思路,领域模型存在于内存对象里,这些对象最终都要落到数据库,由于摆脱了领域模型的束缚,数据库设计是灵活多变的。在盒马,domain object 是怎么进入到数据库的呢。
在盒马,我们设计了 Tunnel 这个独特的接口,通过这个接口我们可以实现对 domain 对象在不同类型数据库的存取。Repository 并没有直接进行持久化工作,而是将 domain 对象转换成 POJO 交给 Tunnel 去做持久化工作,Tunnel 具体可以在任何包实现,这样,部署上,domain 领域模型(domain objects+repositories)和持久化 (Tunnels) 完全的分开,domain 包成为了单纯的内存对象集。
6. 领域模型:部署架构
盒马业务具有很强的整体性:从供应商采购,到商品快递到用户手上,对象之间关系是比较明确的,原则上可以采用一个大而全的领域模型,也可以运用 boundedContext 方式拆分子域,并在交接处处理好数据传送,这里引用老马的一幅图:
我个人倾向于大 domain 的做法,我倾向(所以实际情况不是这样的)的部署结构是:
盒马在架构设计上还在做更多的探索,在 2B+ 互联网的崭新业务模式下,有很多可以深入探讨的细节。DDD 在盒马已经迈出了坚实的第一步,并且在业务扩展性和系统稳定性上经受了实战的考验。基于互联网分布式的工作流引擎(Noble),完全互联网的图形绘制引擎(Ivy)都在精心打磨中,期待在未来的就几个月里,盒马工程师们给大家奉献更多的设计作品。
张群辉,阿里盒马架构总监。10 多年技术及管理实战经验,前阿里基础机构事业部工程效率总监,长期在一线指导大型复杂系统的架构设计。DevOps、微服务架构及领域驱动设计国内最早的实践者一员。崇尚实践出真知,一直奋斗在技术一线。
感谢雨多田光对本文的策划和审校。
来自:http://www.infoq.com/cn/articles/alibaba-freshhema-ddd-practice
相关文章:
全部评论: 0 条
Copyright (C)Demoso 黔ICP备号
Powered By

我要回帖

更多关于 java 充血模型 的文章

 

随机推荐