架构师修行录

专业解读:DDD充血模型最佳实践

大家好,我是Jensen,见字如面。
想来已有五个月没更新,每天被公司各种项目硬控住,凭着真实践才有干货的原则,年前再憋点干货给大家。
最近半年我都在做共享租赁业务,比如美团共享充电宝(越南市场)、共享洗衣机/烘干机(越南)、共享换电柜(国内)、共享净水器(国内&国际)等等,围绕着“租赁”业务去开拓国内与国际SaaS市场,其中就大量用到充血模型来优化代码架构,也是迄今为止我在公司搭建十多个DDD工程最满意的“作品”。
所以今天给大家分享的主题是充血模型,不敢私藏。

0x1

什么是充血模型

充血模型是一种面向对象的软件设计方法,属于领域驱动设计(DDD)的核心概念之一。它强调将业务逻辑和行为封装在领域对象内部,使对象不仅包含数据,还包含与数据相关的操作和业务规则。

核心特点

  1. 封装数据与行为:

  • 在充血模型中,领域对象(如实体或值对象)不仅包含数据属性,还包含与这些数据相关的业务逻辑和行为。例如,一个订单对象不仅包含订单号、客户ID等属性,还包含计算总价、检查库存等方法。

  • 这种设计更符合面向对象编程的核心原则,如封装和单一职责。

  • 高内聚、低耦合:

    • 由于业务逻辑被封装在领域对象内部,对象之间的耦合度降低,系统的可维护性和可扩展性增强。

  • 领域逻辑的自主性:

    • 领域对象能够自主管理自己的状态和行为,减少了对外部服务层的依赖。

    应用场景

    充血模型适用于业务逻辑复杂且需要高度封装的系统。例如,在电商系统中,订单对象可以包含计算总价、检查库存等方法,而不是将这些逻辑放在外部的服务层。

    与贫血模型的对比

    • 贫血模型:

      • 贫血模型是一种将数据和业务逻辑分离的设计模式。领域对象只包含数据属性,而业务逻辑则放在服务层中。

      • 这种设计简单易懂,但在业务逻辑复杂时,服务层可能会变得过于庞大,难以维护。

    • 充血模型:

      • 充血模型将数据和业务逻辑封装在同一对象中,更符合面向对象的设计原则。

      • 它能够更好地利用面向对象的封装特性,使代码更易于扩展和维护。

    相信有不少老铁都接触过贫血模型的工程,在Service类写了几千上万行代码,看个逻辑特费劲,改个小需求都要梳理很久才敢动里面的核心代码,这对有代码洁癖的程序猿太不友好了。

    0x2

    计费租赁领域建模

    以实际项目出发,首先DDD领域建模是常规操作了,梳理好要怎么做这个租赁业务,代码才写得更快些:
    Image
    在计费租赁这个聚合里,核心业务就是:
    1. 后台建计费模板
    2. 投放设备:选设备、点位(门店)、计费模板,把设备投放到某个点位
    3. C端下计费订单,先付后用模式要先走支付流程,充电宝/换电柜要先下押金单并支付(这种有子设备的情况,交了押金才能往下走)
    4. 使用设备,发指令给终端硬件启动,比如充电宝要下发弹宝指令,净水器要下发开水指令,换电柜要下发开仓指令等等
    5. 使用设备结束,计费订单结算扣费
    大致流程如此,建模完事后发现诶,不难,可能麻烦点在于第四点——怎么让一个下单流程支持不同的策略,发不同指令,这个稍后也会说明。
    接下来要用DDD落地了。

    0x3

    共享租赁DDD工程落地

    先上代码结构:
    Image
    还是这套熟悉的经典四层DDD架构,百用不爽,感兴趣可以回看我之前写的DDD四层微服务架构,有变动的是application.factory包,换成了listener包,放事件监听器,主要用于解耦。
    其中参与计费租赁业务的就只有核心的这些类:
    Image
    回到正题,什么情况下要用到充血模型?
    先看最核心的计费订单应用服务:
    Image
    正常来说整个计费业务肯定不止上面620行代码就能写完的,但是这里我用了充血模型,把很多业务逻辑抽离出去了,抽出去的原则很简单:1.原子性的 2.可复用的。我们既可以抽象成静态方法,也可以抽象为领域模型的成员方法。
    比如计费订单的充血方法:
    Image
    但像下单、支付回调、结单、定时任务这些逻辑,抽象为充血模型的方法就不太适合了,还是以贫血模型的思路去做,在应用服务里做。
    有细心的朋友就会问了:在Spring工程下,方法内需要依赖其他Bean怎么办啊?模型的成员变量总不能@Autowired好几个仓库或Mapper吧,不合适。
    这位朋友问得非常好,我以前也有这个困惑,总觉得充血方法不能做CURD,只能写一些简单逻辑,但自从我把D3Boot基础框架搭好以后,这个问题早已经成为过去式了,看看我这里是如何写的:
    Image
    看吧, 根本不需要@Autowired别的Bean,查询就是一行代码的事,查不到还能直接抛错返回友好提示给前端了,save也是一行代码搞定,update封装后也可以updateById,或支持update+where条件,当然,我们也可以在方法内部发Spring封装的领域事件到另一个地方做处理。
    需要这套DDD基础框架的在公众号后台回复d3boot免费领取哈。

    0x4

    领域事件解耦

    回到上面说的listener目录,它作为领域事件或外部MQ事件的入口,做着解耦的事,在这个计费租赁里也发挥了很大的作用。
    试想一下,要对接不同的产品计费下单,传统的方式,要么拆开不同的下单方法前端调不同的接口,要么在下单方法里很N多个ifelse去判断,高级点的就再抽象个策略模式去处理。
    NoNoNo,太麻烦了,在DDD中,领域事件就是干这个事的,计费订单下单只做它领域范围内的事情,发什么指令去给设备,不应该让它操心,于是领域事件就派上用场了:
    Image
    比如在提交订单后,有不同的处理逻辑,那么发个事件吧,不同的租户自己去监听,做不同的事情,还能让监听者自己决定是同步做还是异步做:
    Image
    净水器的业务,客户下完单,净水器监听器去发个指令给设备,就能去打水啦,这里我支持了所有净水器的厂商,都是同一套逻辑:
    Image
    咳咳~如有雷同,纯属雷同。
    关于领域事件这里就不再具体展开了,感兴趣可以看我之前分享的“解耦神器”之SpringEvents领域事件。

    0x5

    写在最后

    充血模型也并不是万能的,因为我这里业务相对简单,我没考虑加事务的情况,有些模型充血方法如果是静态方法要考虑事务的话,还得加一些显式的事务代码,这个在做的过程中遇到问题再优化了,软件工程领域,迭代思维很重要,小步快跑见效果,就是最好的架构演进。
    祝大家新年画蛇添福Image

    EOF

    Image

    作者:Jensen

    专注分享程序员日常·架构技术·职场干货

    Java老兵一枚,深耕电商/IoT/康美/共享租赁/AI等领域产品研发多年。

    D3Boot开源框架作者,现任职SaaS软件架构师。

    关注回复“DDD”,免费领取DDD学习大礼包。

    关注回复“进群”,我拉你进架构技术交流群。

    点赞在看ᴸᵉᵗᴹᵉ年薪百万