大家好,我是Jensen,见字如面。
想来已有五个月没更新,每天被公司各种项目硬控住,凭着真实践才有干货的原则,年前再憋点干货给大家。
最近半年我都在做共享租赁业务,比如美团共享充电宝(越南市场)、共享洗衣机/烘干机(越南)、共享换电柜(国内)、共享净水器(国内&国际)等等,围绕着“租赁”业务去开拓国内与国际SaaS市场,其中就大量用到充血模型来优化代码架构,也是迄今为止我在公司搭建十多个DDD工程最满意的“作品”。
所以今天给大家分享的主题是充血模型,不敢私藏。
一、什么是充血模型
充血模型是一种面向对象的软件设计方法,属于领域驱动设计(DDD)的核心概念之一。它强调将业务逻辑和行为封装在领域对象内部,使对象不仅包含数据,还包含与数据相关的操作和业务规则。
核心特点
- 封装数据与行为:
- 在充血模型中,领域对象(如实体或值对象)不仅包含数据属性,还包含与这些数据相关的业务逻辑和行为。例如,一个订单对象不仅包含订单号、客户ID等属性,还包含计算总价、检查库存等方法。
- 这种设计更符合面向对象编程的核心原则,如封装和单一职责。
- 高内聚、低耦合:
由于业务逻辑被封装在领域对象内部,对象之间的耦合度降低,系统的可维护性和可扩展性增强。
领域逻辑的自主性:
领域对象能够自主管理自己的状态和行为,减少了对外部服务层的依赖。
应用场景
充血模型适用于业务逻辑复杂且需要高度封装的系统。例如,在电商系统中,订单对象可以包含计算总价、检查库存等方法,而不是将这些逻辑放在外部的服务层。
与贫血模型的对比
- 贫血模型:
a.贫血模型是一种将数据和业务逻辑分离的设计模式。领域对象只包含数据属性,而业务逻辑则放在服务层中。
b.这种设计简单易懂,但在业务逻辑复杂时,服务层可能会变得过于庞大,难以维护。
- 充血模型:
充血模型将数据和业务逻辑封装在同一对象中,更符合面向对象的设计原则。
它能够更好地利用面向对象的封装特性,使代码更易于扩展和维护。
相信有不少老铁都接触过贫血模型的工程,在Service类写了几千上万行代码,看个逻辑特费劲,改个小需求都要梳理很久才敢动里面的核心代码,这对有代码洁癖的程序猿太不友好了。
二、计费租赁领域建模
以实际项目出发,首先DDD领域建模是常规操作了,梳理好要怎么做这个租赁业务,代码才写得更快些:
在计费租赁这个聚合里,核心业务就是:
- 后台建计费模板
- 投放设备:选设备、点位(门店)、计费模板,把设备投放到某个点位
- C端下计费订单,先付后用模式要先走支付流程,充电宝/换电柜要先下押金单并支付(这种有子设备的情况,交了押金才能往下走)
- 使用设备,发指令给终端硬件启动,比如充电宝要下发弹宝指令,净水器要下发开水指令,换电柜要下发开仓指令等等
- 使用设备结束,计费订单结算扣费
大致流程如此,建模完事后发现诶,不难,可能麻烦点在于第四点——怎么让一个下单流程支持不同的策略,发不同指令,这个稍后也会说明。
接下来要用DDD落地了。
三、共享租赁DDD工程落地
先上代码结构:
还是这套熟悉的经典四层DDD架构,百用不爽,感兴趣可以回看我之前写的DDD四层微服务架构,有变动的是application.factory包,换成了listener包,放事件监听器,主要用于解耦。
其中参与计费租赁业务的就只有核心的这些类:
回到正题,什么情况下要用到充血模型?
先看最核心的计费订单应用服务:
正常来说整个计费业务肯定不止上面620行代码就能写完的,但是这里我用了充血模型,把很多业务逻辑抽离出去了,抽出去的原则很简单:1.原子性的 2.可复用的。我们既可以抽象成静态方法,也可以抽象为领域模型的成员方法。
比如计费订单的充血方法:
但像下单、支付回调、结单、定时任务这些逻辑,抽象为充血模型的方法就不太适合了,还是以贫血模型的思路去做,在应用服务里做。
有细心的朋友就会问了:在Spring工程下,方法内需要依赖其他Bean怎么办啊?模型的成员变量总不能@Autowired好几个仓库或Mapper吧,不合适。
这位朋友问得非常好,我以前也有这个困惑,总觉得充血方法不能做CURD,只能写一些简单逻辑,但自从我把D3Boot基础框架搭好以后,这个问题早已经成为过去式了,看看我这里是如何写的:
看吧, 根本不需要@Autowired别的Bean,查询就是一行代码的事,查不到还能直接抛错返回友好提示给前端了,save也是一行代码搞定,update封装后也可以updateById,或支持update+where条件,当然,我们也可以在方法内部发Spring封装的领域事件到另一个地方做处理。
需要这套DDD基础框架的在公众号后台回复d3boot免费领取哈。
四、领域事件解耦
回到上面说的listener目录,它作为领域事件或外部MQ事件的入口,做着解耦的事,在这个计费租赁里也发挥了很大的作用。
试想一下,要对接不同的产品计费下单,传统的方式,要么拆开不同的下单方法前端调不同的接口,要么在下单方法里很N多个ifelse去判断,高级点的就再抽象个策略模式去处理。
NoNoNo,太麻烦了,在DDD中,领域事件就是干这个事的,计费订单下单只做它领域范围内的事情,发什么指令去给设备,不应该让它操心,于是领域事件就派上用场了:
比如在提交订单后,有不同的处理逻辑,那么发个事件吧,不同的租户自己去监听,做不同的事情,还能让监听者自己决定是同步做还是异步做:
净水器的业务,客户下完单,净水器监听器去发个指令给设备,就能去打水啦,这里我支持了所有净水器的厂商,都是同一套逻辑:
咳咳~如有雷同,纯属雷同。
五、写在最后
充血模型也并不是万能的,因为我这里业务相对简单,我没考虑加事务的情况,有些模型充血方法如果是静态方法要考虑事务的话,还得加一些显式的事务代码,这个在做的过程中遇到问题再优化了,软件工程领域,迭代思维很重要,小步快跑见效果,就是最好的架构演进。