当前位置: 首页 > 科技观察

如何选择充血模型和贫血模型

时间:2023-03-21 12:02:09 科技观察

从领域模型出发,回顾一下我们进行领域建模的过程:进行需求分析,进行用例设计,对用例进行领域建模,进行数据库设计和程序设计域模型并行。经过前面几步的分析,我们就会得到领域模型以及它们之间的关系。之后,我们要根据领域模型分别进行数据库设计和程序设计。我们会根据领域模型之间的关系,将模型之间的关系映射到系统表设计之间的关系。那么我们应该如何进行相应的程序设计呢?一般来说,将领域模型设计转化为程序设计的方法有两种:贫血模型和充血模型。贫血模型在上一篇文章中,我们给出了一个订单模型的例子:在这个例子中,我们通过设计和抽象得到了一个订单模型。根据这个模型,我们可以直接将模型中的属性提取到模型设计中,将模型中的方法能力提取到服务设计中。也就是说:一个只由属性及其setter和getter组成的对象被称为贫血模型。那么如果你使用贫血模型对领域模型进行编程,你将得到一个领域模型实体对象和服务,就像上图中的例子。实体对象将包含模型对象的所有属性和数据,而服务将包含域模型中的所有方法。当我们要使用领域模型中的方法时,我们将模型实体作为参数,在服务中调用领域模型对应的方法。使用贫血模型建模后,将原有领域模型中的数据和方法拆分为两个对象。而这种拆分使得原本封装在一个对象中的内容分离成了两个对象。它打破了原有领域模型的封装。封装的破除会带来新的问题。我举一个我在实际生产中遇到的例子:如图所示,如果我们有两个订单的子对象:已结算商户订单和自营订单。这两个订单的下单方式有不同的业务流程。如果使用贫血模型设计,我们很容易想到通过两个服务来实现两个不同的订单业务流程。相应地,为了区分不同的服务,需要将订单分为两个对象,将指定的对象作为入参,调用相应的服务。而这样设计的时候,在订单的上游需要一个业务编排层来处理订单数据的流转,保证相应的对象不会被误录入。按照这种设计思路,如果在此基础上增加新的“分发单”。我们需要调整三个地方:添加配送订单对象、添加配送订单服务、调整上游编排层。显然,这不符合开闭原则,同时,业务发展的成本也增加了。注意:我们可以通过设计模式来优化最终实现的逻辑,但是设计思路是这样的。拥塞模型贫血模型的问题在于它拆分了领域模型的封装,所以不是拆分模型的封装,而是保留领域模型的原貌进行编程,就是拥塞模型。可以描述为:直接在领域对象中实现领域模型中的方法就是充血模型设计。根据上面的订单领域模型,如果我们换成充血模型设计,就变成了:可以看到,如果采用充血模型设计,订单的实体对象包含了领域模型中的方法,即place订单的实际逻辑是由订单对象本身完成的。至于订单对象中的子对象,只需要继承订单对象,然后实现自己的下单方法即可。使用拥塞模型设计后,我们发现订单对应的还有一个服务,但是这个服务只是执行了一个调用订单对象中对应逻辑的函数,而这样的服务并不关心调用了哪个子类。因此,我们采用充血模型设计。当需要添加新的“配送订单”时,我们只需要新建订单的子类,然后在该类中实现相应的订单逻辑即可。这符合开闭原则。并且由于充血模型还原了领域模型的原貌,在按照领域模型编程时,其映射关系直观,相应的代码修改也更加直接。AnemiaModelVSCongestionModel通过以上分析,充血模型总体上优于贫血模型。但是在实际的开发应用中并不是这样的。贫血模型比充血模型更容易实施。由于充血模型还原了领域模型的原貌,因此在运行程序时需要将模型下的所有组合和聚合对象组装起来。以一个订单为例,该订单需要封装:订单、订单详情、用户、用户地址、商品等5个对象。由于封装的复杂性,还需要设计订单仓库、订单工厂等组件来创建订单对象。同时,由于创建对象的大小可能比较大,订单仓库中一般需要进行缓存设计。总的来说:充血模型依赖强大的技术平台来维护模型的使用。并且因为贫血模型是通过划分模型来实现的,所以在操作订单的时候,只需要根据需要操作订单和订单明细即可。Controller、Service、Dao通用的三层设计,可以支持订单的模型设计。由于拆分后的模型数据不必组装成领域模型对象,Dao查询完数据后,可以直接返回给Service使用,反之亦然。系统的复杂性大大降低。贫血模型简单明了,可以直接在经典的三层中实现。充血模型需要有更强的设计能力。由于充血模型是领域模型的直接表示,领域模型设计的好坏将直接影响系统的整体性能和可扩展性。这就需要开发者有更强的对象分析和对象设计能力。同时,由于不同领域的数据需要在充血模型中聚合,团队之间可能需要提出数据查询需求,这就需要更强的团队协作能力。相反,贫血模型的所有业务流程都在服务中运行,模型被分割,因此对数据的外部依赖更少,层次也更少。大部分逻辑都是查询数据库后直接返回,对开发者的能力要求不高。贫血模型可以逐步编程。当面对一长串复杂的业务场景时,我们可能更倾向于将业务拆解成多个串联的步骤,然后独立执行。在使用贫血模型的情况下,可以直接通过编写多个服务来进行面向步骤的编程。每个Service都可以处理对象中的部分数据,经过多个Service处理后得到最终的结果。虽然在方法上也可以拆分充血模型,但它不像贫血模型那样直接。贫血模型和充血模型尽管上面对充血模型和贫血模型进行了直接区分,但这两种设计并不是二元的。简单来说,我们可以根据充血模型和贫血模型的特点来选择我们需要使用的部分。例如:把需要封装的业务逻辑放到领域对象中,按照拥塞模型设计,把不需要封装的业务逻辑放到Service中,按照贫血模型设计,那么设计对于上面的订单模型就变成了:也就是说,只需要封装的订单逻辑放在“进驻商户订单”和“自营订单”中,而支付和查询订单状态的逻辑仍然放在在服务中进行主要处理。其实封装的概念比较模糊,但基本上可以参考:有继承和多态关系:比如订单和自营订单需要进行编码转换:比如根据value需要反映必填字段Relevance:比如订单和订单明细之间的关系。最后,经过合理分析,得出贫血模型和充血模型应该根据特点混合使用的结论。但是在实际企业中,如何判断哪些方法可以放入模型中的标准是比较模糊的。也就是说,更多的还是需要靠架构设计、个人开发能力、codereview,而以上三者对小而年轻的团队并不是很友好。所以我认为为了提高下限,使用贫血模型更安全。