專業解讀:DDD充血模型優秀實踐
大家好,我是Jensen,見字如面。
想來已有五個月沒更新,每天被公司各種項目硬控住,憑著真實踐才有干貨的原則,年前再憋點干貨給大家。
最近半年我都在做共享租賃業務,比如美團共享充電寶(越南市場)、共享洗衣機/烘干機(越南)、共享換電柜(國內)、共享凈水器(國內&國際)等等,圍繞著“租賃”業務去開拓國內與國際SaaS市場,其中就大量用到充血模型來優化代碼架構,也是迄今為止我在公司搭建十多個DDD工程最滿意的“作品”。
所以今天給大家分享的主題是充血模型,不敢私藏。
一、什么是充血模型
充血模型是一種面向對象的軟件設計方法,屬于領域驅動設計(DDD)的核心概念之一。它強調將業務邏輯和行為封裝在領域對象內部,使對象不僅包含數據,還包含與數據相關的操作和業務規則。
核心特點
- 封裝數據與行為:
- 在充血模型中,領域對象(如實體或值對象)不僅包含數據屬性,還包含與這些數據相關的業務邏輯和行為。例如,一個訂單對象不僅包含訂單號、客戶ID等屬性,還包含計算總價、檢查庫存等方法。
- 這種設計更符合面向對象編程的核心原則,如封裝和單一職責。
- 高內聚、低耦合:
- 由于業務邏輯被封裝在領域對象內部,對象之間的耦合度降低,系統的可維護性和可擴展性增強。
- 領域邏輯的自主性:
- 領域對象能夠自主管理自己的狀態和行為,減少了對外部服務層的依賴。
應用場景
充血模型適用于業務邏輯復雜且需要高度封裝的系統。例如,在電商系統中,訂單對象可以包含計算總價、檢查庫存等方法,而不是將這些邏輯放在外部的服務層。
與貧血模型的對比
- 貧血模型:
a.貧血模型是一種將數據和業務邏輯分離的設計模式。領域對象只包含數據屬性,而業務邏輯則放在服務層中。
b.這種設計簡單易懂,但在業務邏輯復雜時,服務層可能會變得過于龐大,難以維護。
- 充血模型:
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中,領域事件就是干這個事的,計費訂單下單只做它領域范圍內的事情,發什么指令去給設備,不應該讓它操心,于是領域事件就派上用場了:
比如在提交訂單后,有不同的處理邏輯,那么發個事件吧,不同的租戶自己去監聽,做不同的事情,還能讓監聽者自己決定是同步做還是異步做:
凈水器的業務,客戶下完單,凈水器監聽器去發個指令給設備,就能去打水啦,這里我支持了所有凈水器的廠商,都是同一套邏輯:
咳咳~如有雷同,純屬雷同。
五、寫在最后
充血模型也并不是萬能的,因為我這里業務相對簡單,我沒考慮加事務的情況,有些模型充血方法如果是靜態方法要考慮事務的話,還得加一些顯式的事務代碼,這個在做的過程中遇到問題再優化了,軟件工程領域,迭代思維很重要,小步快跑見效果,就是最好的架構演進。