架构真意:企业级应用架构设计方法论与实践
上QQ阅读APP看书,第一时间看更新

3.3.2 贫血模型与充血模型

服务、实体与值对象,是领域驱动设计的基本元素。然而,要将业务领域模型最终转换为程序设计,还要加入相应的设计。通常,将业务领域模型转换为程序设计,有两种设计思路:贫血模型与充血模型。

软件大师Martin Fowler在自己的博客中提出了“贫血模型”的概念,当时是将其作为反模式来批评的。所谓的“贫血模型”,就是在软件设计中有很多的POJO(Plain Ordinary Java Object)对象,它们除了有一堆get/set方法外,几乎没有任何业务逻辑。这样的设计被称为“贫血模型”,如图3-13所示。

我们以图3-13为例,在领域模型中有VIP的领域对象除了有很多属性以外,还有“会员打折”“会员福利”“会员特权”等方法。如果将该领域模型按照贫血模型进行设计,就会设计一个VIP的实体对象与Service。实体对象包含该对象的所有属性以及这些属性包含的数据。然后,将所有的方法都放入Service中,在调用它们的时候,必须将领域对象作为参数进行传输。在这样的设计中,领域对象中的那些方法及其在执行过程中所需的数据被割裂到两个不同的对象中,对象的封装性被打破了。这么做会带来什么问题呢?

图3-13 VIP的贫血模型设计

如图3-14所示,在领域模型中的VIP通过继承分为了“金卡会员”与“银卡会员”。如果将该领域模型按照贫血模型进行设计,则会设计出一个“金卡会员”的实体对象与Service,同时设计出一个“银卡会员”的实体对象与Service。“金卡会员”的实体对象应当调用“金卡会员”的Service,如果用“金卡会员”的实体对象去调用“银卡会员”的Service,系统就会出错。所以,除了进行以上的设计之外,还需要有一个客户程序去判断当前的实体对象是“金卡会员”还是“银卡会员”。这时,系统变更就不那么灵活了。

图3-14 VIP的贫血模型设计

如果现在需要在原有的基础上再增加一个“铂金会员”,那么不仅要增加一个“铂金会员”的实体对象与Service,还要修改客户程序的判断,系统变更成本就会提高。

针对贫血模型的问题,Martin Fowler提出了“充血模型”的概念。如图3-15所示,假如采用“充血模型”,就会将领域对象的原貌直接转换为实体对象的设计。因此在程序设计时,既有父类的“VIP”,又有子类“金卡会员”与“银卡会员”。与贫血模型不同的是,那些在领域对象中的方法也同样保留到了程序设计的实体对象中。这样,通过继承,虽然“金卡会员”与“银卡会员”都有“会员打折”,但“金卡会员”的“会员打折”与“银卡会员”的“会员打折”是不一样的。

图3-15 VIP的充血模型设计

与贫血模型不同的是,虽然在充血模型中也有Service,里面也有“会员打折”“会员福利”“会员特权”等方法,但是充血模型的Service只做一件非常简单的事,就是在接收到用户的请求后,直接去调用实体对象中的相应方法,其他的什么都不做。这样,VIP Service不需要关注现在调用的是“金卡会员”还是“银卡会员”,它只需要去调用“会员打折”就行了。如果当前拿到的是“金卡会员”,就是执行“金卡会员”的“会员打折”;如果当前拿到的是“银卡会员”,就是执行“银卡会员”的“会员打折”。如果要再增加一个“铂金会员”,就只需要写一个“铂金会员”的子类,重写“会员打折”方法,而VIP Service不需要做任何修改,变更成本就大大降低了。

采用充血模型的设计有诸多好处。充血模型保持了领域模型的原貌,领域模型可直接转换成程序的设计。当领域模型随着业务变更而频繁甚至大幅度调整时,可以比较直接地映射成程序的变更,代码修改起来比较直接。此外,充血模型保持了对象的封装性,使得领域模型在面临多态、继承等复杂结构时易于变更。

充血模型在理论上非常优雅,在工程实践上却不尽如人意。而贫血模型从表面上看简单粗暴,但在工程实践上有许多优异的特性。主要体现在以下几个方面。

(1)贫血模型比充血模型更加简单易行

充血模型是将领域模型的原貌直接映射成了程序设计,因此在程序设计时需要增加更多的诸如仓库、工厂的组件,对设计能力与架构提出了更高的要求。譬如,现在要设计一个订单系统,在领域建模时,每个订单需要有多个订单明细,还要对应相关的客户信息、商品信息。因此,在装载一个订单时,需要同时查出它的订单明细和对应的客户信息、商品信息。这些需要有强大的订单工厂进行装配。装载订单以后,还需要放到仓库中进行缓存,因此需要订单仓库具有缓存的能力。此外,在保存订单的时候,还需要同时保存订单和订单明细,并将它们放到一个事务中。所有这些都需要有强力的技术平台的支持(详见4.3.3节)。这个系统的充血模型架构设计如图3-16所示。

图3-16 充血模型的架构设计

相反,贫血模型就显得更加简单。在贫血模型中,MVC层直接调用Service,Service通过DAO进行数据访问。在这个过程中,每个DAO都只查询数据库中的某个表,然后直接交给Service去使用,去完成各种处理。以订单系统为例,订单有订单DAO,负责查询订单;订单明细有订单明细DAO,负责查询订单明细。它们查询出来以后,不需要装配,而是直接交给订单Service使用。在保存订单时,订单DAO负责保存订单,订单明细DAO负责保存订单明细。它们都是通过订单Service进行组织,并建立事务。贫血模型不需要仓库,不需要工厂,也不需要缓存,一切都显得那么简单且一目了然,如图3-17所示。

图3-17 贫血模型的架构设计

(2)充血模型需要有更强的设计与协作能力

充血模型的设计实现对开发人员的能力提出了更高的要求,开发人员需要具有更强的OOA/D能力,分析业务、业务建模与设计能力。譬如在订单系统这个案例中,开发人员需要首先进行领域建模,分析清楚该场景中的订单、订单明细、用户、商品等领域对象的关联关系,还需要分析各个领域对象在真实世界中都有什么行为,对应到软件设计中都有什么方法,在此基础上再进行设计开发。

同时,充血模型需要有较强的团队协作的能力。比如,在该场景的订单创建过程中,需要对用户以及用户地址的相关信息进行查询,这时候订单Service不能直接去查询用户和用户地址的相关表,而是要去调用用户Service的相关接口,由用户Service去完成对用户相关表的查询。开发订单模块的团队就需要向开发用户模块的团队提出接口需求。

与充血模型相比,贫血模型就比较简单直接。所有业务处理过程都交给Service去完成。在业务处理过程中,需要哪些表的数据,就去调用相应的DAO:需要订单就找订单DAO,需要用户就找用户DAO,需要商品就找商品DAO。程序简单就易于理解,日后维护起来也比较容易。

(3)贫血模型更容易应对复杂的业务处理场景

在进行设计时,充血模型是在领域对象的相应方法中实现所有业务处理过程的。如果业务处理过程比较简单,这样的设计还可以从容应对,但如果面对的是非常复杂的业务处理场景,就有一些力不从心。

在这些复杂的业务处理场景中,如果采用贫血模型,可以将复杂的业务处理场景划分成多个相对独立的步骤,然后将这些独立的步骤分配给多个Service串联起来执行。这样,各个步骤就以一种松耦合的形式串联在一起,以领域对象作为参数在各个Service中进行传递,如图3-18所示。

图3-18 复杂业务流程的生产流水线处理方式

在这样的设计中,领域对象既可以作为各个方法调用的输入,又可以作为它们的输出。比如,在这个案例中,领域对象作为参数首先调用ServiceA,之后将结果数据写入领域对象的前5个字段,传递给ServiceB。ServiceB拿到领域对象以后,既可以作为输入去读取前5个字段,又可以作为输出将执行结果写入中间5个字段。最后,将领域对象传递给ServiceC,执行完操作以后去写后面5个字段。当所有字段都写入完成以后,存入数据库,完成所有操作。

在这个设计中,如果日后需要变更,要增加一个处理过程,或者去掉一个处理过程,再或者调整它们的执行顺序,都是比较容易的。这样的设计要求处理过程必须在领域对象之外,在Service中实现。然而,如果采用的是充血模型的设计,就必须要将所有的处理过程都写入这个领域对象中去实现,不管这些处理过程有多复杂。这样的设计势必会加大日后变更维护的成本。

所以,贫血模型和充血模型各有优缺点。到底应当采用贫血模型还是充血模型,人们争执了多年,但笔者认为它们并不是熊掌和鱼的关系。我们应当把它们结合起来,取长补短,合理利用。首先应当弄清楚贫血模型和充血模型的差别,也就是两种模型的业务逻辑应当在哪里实现:贫血模型的业务逻辑在Service中实现,但充血模型是在领域对象中实现的。清楚了这一点,那么在以后的软件设计中,我们可以将那些需要封装的业务逻辑放到领域对象中,按照充血模型去设计。其他的业务逻辑放到Service中,按照贫血模型去设计。

那么,哪些业务逻辑需要封装起来按照充血模型设计呢?这个问题仁者见仁智者见智,本书总结了以下几点经验,供读者参考。

1)如前所述,如果在领域模型中出现了类似继承、多态的情况,则应当将继承与多态的部分以充血模型的形式在领域对象中实现。

2)如果软件设计的过程中需要对一些类型或者编码进行转换,则将转换的部分封装在领域对象中。例如一些布尔类型的字段,在数据库中是没有布尔类型的,不同的人习惯不同,有的人习惯采用0和1,有的人习惯用Y和N,或者T和F,这样就会给上层开发人员带来诸多的困惑,到底哪些字段是Y和N,哪些是T和F。这时我们就可以将它们封装在领域对象中,然后转换为布尔类型展现给上层开发人员,按充血模型来设计。

3)希望在软件设计中能更好地表现领域对象之间的关系。比如,在查询订单的时候显示每个订单对应的用户,以及每个订单包含的订单明细。这时,除了要将领域模型中的关系体现在领域对象的设计上之外,还需要有仓库与工厂的支持。如装载订单时需要同时查询订单和订单明细,并通过订单工厂装配;查询订单以后需要通过工厂补填相应的用户与明细。

4)在“聚合”情况下。“聚合”就是在真实世界中那些代表整体与部分的事物之间的关系。比如,在订单中有订单和订单明细,一个订单对应多个订单明细。从业务关系来说,它们是整体与部分的关系,订单明细是订单的一个部分,没有了这张订单,它的订单明细就没有任何意义了。这时,我们在操作订单的时候,就应当将对订单明细的操作封装在订单对象中,按照充血模型的形式进行设计。