一文讀懂 DDD 領域驅動設計

來源:CSDN

作者:靖節先生

原文鏈接:https://blog.csdn.net/m0_37583655/article/details/117565641

1. 領域驅動概述

1.1 領域驅動簡介

領域驅動設計是 Eric Evans 在 2004 年發表的 Domain Driven Design(領域驅動設計,DDD) 著作中提出的一種從系統分析到軟件建模的一套方法論。以領域爲核心驅動力的設計體系。

從領域驅動定義來看,領域驅動設計 - 軟件核心複雜性應對之道,從 Eric 定義中可以看出,領域驅動設計是爲了解決複雜的軟件設計,而且只是解決軟件複雜性的一種方式,並不是唯一選擇。另外不是所有的業務服務都合適做 DDD 架構,DDD 適合產品化,可持續迭代,業務邏輯足夠複雜的業務系統,對於系統初期業務邏輯相對比較簡單的應用,傳統 MVC 架構更具有優勢,可以減少一部分認知成本與開發成本。而且領域驅動設計並不是萬金油,只是解決複雜軟件的一種方案,領域驅動設計本身只提供了理論思想,具體的落地方案一定是結合具體的業務場景實現的。目前市面上也有很多依據領域驅動思想落地的開源框架可以參考。

從領域驅動對應關係來看,一方面目前很多建設中臺的時候大多采用 DDD 思想落地,DDD 很多思想比如領域劃分,領域事件,領域服務,邊界上下文劃分,充血模型,代碼防腐,統一語義等等可以很好的幫助實現中臺的落地,但是中臺落地 DDD 並不是唯一選擇。另一方面對於 DDD 的這些思想,與 DDD 的關係更多是聚合關係,而不是組合關係,也就是在具體應用開發中,即使採用傳統的 MVC 架構,這些思想依然可以很好的發揮其作用。

1.2 領域驅動優點

DDD 最大的好處是:接觸到需求第一步就是考慮領域模型,而不是將其切割成數據和行爲,然後數據用數據庫實現,行爲使用服務實現,最後造成需求的首肢分離。DDD 讓你首先考慮的是業務語言,而不是數據。DDD 強調業務抽象和麪向對象編程,而不是過程式業務邏輯實現。重點不同導致編程世界觀不同。

  1. 面向對象設計,數據行爲綁定,告別貧血模型。

  2. 優先考慮領域模型,而不是切割數據和行爲。

  3. 業務語義顯性化,準確傳達業務規則。

  4. 代碼即設計,通過領域設計即可很清晰的實現代碼。

  5. 它通過邊界劃分將複雜業務領域簡單化,幫我們設計出清晰的領域和應用邊界,可以很容易地實現業務和技術統一的架構演進。

領域驅動設計,又稱 "軟件核心複雜性應對之道"。是一套基於對象思維的業務建模設計思想,相對於 CRUD 系統有更高的靈活性,是業務人員處理複雜問題的有效手段。

通用語言:“一個團隊,一種語言”,將模型作爲語言的支柱。確保團隊在內部的所有交流中,代碼中,畫圖,寫東西,特別是講話的時候都要使用這種語言。例如賬號,轉賬,透支策略,這些都是非常重要的領域概念,如果這些命名都和我們日常討論以及 PRD 中的描述保持一致,將會極大提升代碼的可讀性,減少認知成本。說到這,稍微吐槽一下我們有些工程師的英語水平,有些神翻譯讓一些核心領域概念變得面目全非。

顯性化:就是將隱式的業務邏輯從一推 if-else 裏面抽取出來,用通用語言去命名、去寫代碼、去擴展,讓其變成顯示概念,比如 "透支策略" 這個重要的業務概念,按照事務腳本的寫法,其含義完全淹沒在代碼邏輯中沒有突顯出來,看代碼的人自然也是一臉懵逼,而領域模型裏面將其用策略模式抽象出來,不僅提高了代碼的可讀性,可擴展性也好了很多。

1.3 領域驅動解決複雜度的方式

首先, 典型的 DDD 實現了業務複雜度和技術複雜度的隔離,通過分層架構隔離了關注點,舉個例子,在傳統的 DDD 四層架構中,DDD 劃分出了領域層、倉儲層、基礎設施層、接口層;

在領域層中,存放業務邏輯的關注點,即所謂的領域行爲;在應用層中,DDD 暴露出了 業務用例級別 (Use Case)的服務接口,粘合業務邏輯與技術實現;在基礎設施層中,DDD 集中放置了支撐業務邏輯的技術實現,如:MQ 消息發送、對緩存的操作等;在倉儲層中,DDD 放置了和領域狀態相關的邏輯,打通了領域狀態持久化與存儲設施之間的聯繫。

除了劃分不同分層外,DDD 還提出了一個建設性的概念: “限界上下文(Bounded Context)”,通過限界上下文對業務流程分而治之,切分爲不同的子系統,在每個子系統中利用 DDD 的分層架構 / 六邊形架構等思想分別進行邏輯分層。通過這樣的分治之後,DDD 幫我們將業務複雜度隔離到了每個細分的領域內部,而且 DDD 本身的分治思想,也幫助我們隔離了業務需求和技術需求的關注點。

這是一個典型的領域驅動設計分層架構,藍色區域的內容與業務邏輯有關,而灰色區域的內容則與技術實現有關。這二者涇渭分明,最後匯合在應用層。

應用層確定了業務邏輯與技術實現的邊界,通過直接依賴或者依賴注入(DI,Dependency Injection)的方式將二者結合起來。充分體現了 DDD 能夠隔離技術複雜度與業務複雜度的特點。

2. 領域驅動核心知識

2.1 領域知識概念

DDD 的核心知識體系主要包括領域、子域、核心域、支撐域、通用域、限界上下文、實體、值對象、聚合、聚合根等概念。

2.2 領域戰略戰術設計

DDD 有戰略設計和戰術設計之分。戰略設計主要從高層 "俯視" 我們的軟件系統,幫助我們精準地劃分領域以及處理各個領域之間的關係;而戰術設計則從技術實現的層面教會我們如何具體地實施 DDD。

戰略建模 - Strategic Modeling

限界上下文(Bounded Context)

上下文映射圖(Context Mapping)

戰術建模 - Tactical Modeling:

聚合 - Aggregate

實體 - Entity

值對象 - Value Objects

資源庫 - Repository

領域服務 - Domain Services

領域事件 - Domain Events

模塊 - Modules

3. 領域驅動戰略設計

3.1 戰略設計概述

需要指出的是,DDD 絕非一套單純的技術工具集,但是我所看到的很多程序員卻的確是這麼認爲的,並且也是懷揣着這樣的想法來使用 DDD 的。過於拘泥於技術上的實現將導致 DDD-Lite。簡單來講,DDD-Lite 將導致劣質的領域對象,因爲我們忽略了 DDD 戰略建模所帶來的好處。

DDD 的戰略設計主要包括領域 / 子域、通用語言、限界上下文和架構風格等概念。

3.2 領域與子域

3.3 限界上下文

3.4 領域場景分析

3.5 四色建模法

3.6 事件風暴結果圖

3.7 限界上下文依賴結果圖

4. 領域驅動戰術設計

4.1 戰術設計概述

領域驅動設計,整體包括戰略和戰術兩部分,其中戰略部分的落地需要團隊合作、開發過程、流程制度等一系列支持,實施阻力相對較大。相反,戰術部分,是一組面向業務的設計模式,是基於技術的一種思維方式,相對開發人員來說比較接地氣,是提升個人格局比較好的切入點。

戰略設計爲我們提供一種高層視野來審視我們的軟件系統,而戰術設計則將戰略設計進行具體化和細節化,它主要關注的是技術層面的實施,也是對我們程序員來得最實在的地方。

戰術模式包含若干構造塊模式,以便能夠構建有效的領域模型。

戰術模式嚴重依賴於領域模型和通用語言,通過技術模式將領域模型和通用語言中的概念映射到代碼實現中。隨着模型的進化,代碼實現也會進行重構,以更好的體現模型概念。當然,從技術重構角度也會發現一些隱含領域知識(概念),這些新的發現也會對領域模型產生影響。

戰術模式和通用語言一樣,都工作在特定限界上下文內,其應用邊界受限界上下文的保護。

4.2 戰術模式

戰術模式的作用是管理複雜性並確保領域模型中行爲的清晰明確。可以使用這些模式來捕獲和傳遞領域中的概念、關係、規則。

每個構造塊模式都具有單一職責:

  1. 代表領域中的概念。如實體、值對象、領域服務、領域事件、模塊等;

  2. 用於管理對象的生命週期。如聚合、工廠、倉庫等;

  3. 用於集成或跟蹤。領域事件、事件溯源等。

4.3 領域建模模式

他們表述實現與模型間的關係,將分析模型綁定到代碼實現模型。主要用於在代碼中表述模型元素的模式。

1. 實體

實體表述的是領域中的概念,它是由身份而不是屬性來定義的。

實體的身份標識在生命週期中保持不變,但其屬性會發生變化。實體以身份標識作爲唯一憑證,沿着時間軸,記錄了實體所有變更事件。

實體的一個實例是產品,一旦產品被生成好,其唯一身份就不會發生變化,但是其描述信息、價格等可以被多次修改。

2. 值對象

值對象代表僅通過數據區分的領域元素和概念。用作模型中元素的描述,它不具有唯一標識。

值對象不需要唯一標識,是因爲它總是與另一個對象相關聯,是在一個特定上下文中被解析的。通常,其生命週期會依附於它的關聯對象(在這裏,主要是實體對象)。

值對象會當做不變對象來設計,在完成創建後,其狀態就不能改變了。

值對象比較好的例子就是現金,你無需關係貨幣的身份,只關心它的價值。如果有人用一張五美元鈔票交換你的五張一美元鈔票,也不會改變五美元本身。

3. 領域服務

在模型中,領域服務封裝了不能自然建模爲值對象和實體的邏輯、流程和概念。

它本身不具有身份和狀態。它的職責是使用實體和值對象編排業務邏輯。

領域服務的一個好例子是運輸成本計算器,只要給出一組拖運貨物和重量,它就能計算運輸成本。

4. 模塊

模塊主要用於組織和封裝相關概念(實體、值對象、領域服務、領域事件等),這樣可以簡化對較大模型的理解。

應用模塊可以在領域模型中促成低耦合和搞內聚的設計。

模塊作用於單個領域,用於分解模型規模。子域用於限定領域模型適用範圍(有界上下文)。

4.4 對象生命週期模式

相對來說,之前提到的模式重點在於表達領域概念。而對象生命週期模式,有點側重於技術,用於表示領域對象的創建和持久化。

1. 聚合

實體和值對象會相互協作,形成複雜的關聯關係。我們需要在滿足不變條件的前提下,將其拆分爲一個個概念上的整體。通常,面對複雜的對象關係,在執行領域對象操作時,難以保證一致性和併發性。領域驅動設計由聚合模式來確保操作的一致性和事務的併發邊界。大模型會通過不變性條件來劃分,並組成概念化整體的實體和對象組,這個概念化整體便是聚合。

聚合根之間的關係應該通過保持對另一個聚合根 ID 的引用,而非對對象本身的引用來實現。這一原則有助於保持聚合之間的邊界並避免加載不必要的對象。

不變性,是在領域模型中強制實現一致性的規則。無論何時對實體或聚合進行變更都要應用該業務規則。聚合外部的對象只能引用另一個聚合的聚合根,聚合中對象的任何變更都需要通過聚合根來完成。聚合根封裝聚合數據並公開行爲以對其進行修改。

2. 工廠

如果實體或值對象的創建過程非常複雜,可以將其委託給工廠。工廠會確保在領域對象使用之前就滿足所有的不變條件。

如果領域對象很簡單並且不具有特殊的不變條件,可以使用構造函數代替工廠。當從持久化存儲中重建領域對象時,也可以使用工廠。

3. 倉庫

倉庫主要用於持久化一個聚合。將聚合作爲原子單元進行處理,因此,倉庫操作的最小單元就是聚合,每個聚合會對應一個倉庫。

倉庫是用來檢索和存儲聚合的機制,是對基礎框架的一種抽象。

4.4 其他模式

1. 領域事件

領域事件表示問題空間中發生了一些業務人員關心的事情。主要用於表示領域概念。

使用領域事件主要有以下兩種場景:

  1. 記錄模型的變更歷史;

  2. 作爲跨聚合通信方式。

2. 事件溯源

傳統的僅快照式持久化的一個替代項便是事件溯源。作爲實體狀態存儲的替代,可以存儲引發該狀態的系列事件。存儲所有的事件會提高業務的分析能力,不僅可以得知實體當前狀態,還可以得知過去任意時點的狀態。

4.5 總結

實體
        由唯一標識符定義
        標識符在整個生命週期保存不變
        基於標識符進行相等性檢查
        通過方法對屬性進行更新
值對象
        描述問題域中的概念和特徵
        不具備身份
        不變對象
領域服務
        處理無法放置在實體或值對象中的領域邏輯
        無唯一標識
        無狀態服務
模塊
        分解、組織和提高領域模型的可讀性
        命名空間,降低耦合,提供模型高內聚性
        定義領域對象組間的邊界
        封裝比較獨立的概念,是比聚合、實體等更高層次的抽象
聚合
        將大對象圖分解成小的領域對象羣,降低技術實現的複雜性
        表示領域概念,不僅僅是領域對象集合
        確定領域一致性邊界,確保領域的可靠性
        控制併發邊界
工廠
        將對象的使用和構造分離
        封裝複雜實體和值對象的創建邏輯
        保障複雜實體和值對象的業務完整性
倉庫
        是聚合根在內存中的集合接口
        提供聚合根的檢索和持久化需求
        將領域層與基礎實施層解耦
        通常不用於報告需求
領域事件
        業務人員所關心的事件,是通用語言的一部分
        記錄聚合根的所有變更
        處理跨聚合的通信需求
事件溯源
        使用歷史事件記錄替換快照存儲
        提供對歷史狀態的查詢

5. 領域驅動架構模型

5.1 領域驅動基本架構

5.1.1 分層架構

5.1.2 六邊形理論

5.1.3 CQRS 架構設計

5.2 領域驅動分層架構

1.用戶接口層:面向前端用戶提供服務和數據適配。這一層聚集了接口和數據適配相關的功能。
2.應用層:實現服務組合與編排,主要適應業務流程快速變化的需求。這一層聚集了應用服務和時間訂閱相關的功能。
3.領域層:實現領域模型的核心業務邏輯。這一層聚集了領域模型的聚合、聚合根、實體、值對象、領域服務和領域事件,通過個領域對象的協同和組合形成領域模型的核心業務能力。
4.基礎設施層:它貫穿所有層,爲各層提供基礎資源服務。這一層聚集了各種底層資源相關的服務和能力。

5.3 服務調用

5.4 服務封裝與組合

5.5 領域架構對應關係

6. 領域驅動落地框架

6.1 leave-sample

中臺架構與實現 DDD 和微服務,清晰地提供了從戰略設計到戰術設計以及代碼落地。

leave-sample 地址:https://gitee.com/serpmelon/leave-sample

6.2 dddbook

阿里技術專家詳解 DDD 系列,例子精煉,項目代碼結構與 rdfa 相似,極具參考價值。

dddbook 地址:https://developer.aliyun.com/article/719251

6.3 Xtoon

xtoon-boot 是基於領域驅動設計(DDD)並支持 SaaS 平臺的單體應用開發腳手架。重點研究如何應用。xtoon-boot 提供了完整落地方案和企業級手腳架;

gitee 地址:https://gitee.com/xtoon/xtoon-boot

github 地址:https://github.com/xtoon/xtoon-boot

6.4 DDD Lite

DDD 領域驅動設計微服務簡化版,簡潔、高效、值得重點研究,主要問題是持久化採用的 JPA,擔心技術人員不熟悉,理論篇。

gitee 地址:https://gitee.com/litao851025/geekhalo-ddd

快速入門:https://segmentfault.com/a/1190000018464713

快速構建新聞系統:https://segmentfault.com/a/1190000018254111

6.5 ruoyi_cloud

若依快速開發平臺,以該項目建立對陽光智採和 rdfa 的技術框架基準線。

gitee 地址:https://gitee.com/y_project/RuoYi-Cloud

6.6 Cola 框架

cola 框架是阿里大佬張建飛(Frank) 基於 DDD 構建的平臺應用框架。“讓 COLA 真正成爲應用架構的最佳實踐,幫助廣大的業務技術同學,脫離醬缸代碼的泥潭!”

csdn 地址:https://blog.csdn.net/significantfrank/article/details/110934799

6.7 Axon Framework

Axon Framework 是用來幫助開發人員構建基於命令查詢責任分類 (Command Query Responsibility Segregation: CQRS) 設計模式的可伸縮、可擴展和可維護應用程序的框架。你只需要把工作重心放在業務邏輯的設計上。通過一些 Annotation ,Axon 使得你的代碼和測試分離。

https://www.oschina.net/p/axon

https://www.jianshu.com/p/15484ed1fbde

7. 領域驅動實踐

7.1 貧血模型和充血模型

1. 貧血模型概念

貧血模型,所謂的貧血模型是在定義對象時,指定以對象的屬性信息,卻沒有對象的行爲信息,比如,定義 Employee 對象會包含 id,name,age,sex,role,phone 等信息,最後再通過添加一些對象屬性的 get/set 方法來賦值取值操作。

這些貧血對象在設計之初就被定義爲只能包含數據,不能加入領域邏輯;所有的業務邏輯是放在所謂的業務層(xxxService, xxxManager 對象中),需要使用這些模型來傳遞數據。

2. 充血模型概念

充血模型,在定義對象時不但包含對象的屬性信息,還包括對象的行爲信息。所以充血模型是一種有行爲的模型,模型中狀態的改變只能通過模型上的行爲來觸發,同時所有的約束及業務邏輯都收斂在模型上。

3. 貧血模型和充血模型的區別

貧血模型是事務腳本模式,貧血模型相對簡單,模型上只有數據沒有行爲,業務邏輯由 xxxService、xxxManger 等類來承載,相對來說比較直接,針對簡單的業務,貧血模型可以快速的完成交付,但後期的維護成本比較高,很容易變成我們所說的麪條代碼。

充血模型是領域模型模式,充血模型的實現相對比較複雜,但所有邏輯都由各自的對象來負責,職責比較清晰,方便後期的迭代與維護。充血模型更加符合現實中的對象,因爲一個員工在現實世界裏不只有姓名,年齡,電話等,還可以工作,喫飯,睡覺等行爲,只有屬性信息的對象不是一個完整的對象。

面向對象設計主張將數據和行爲綁定在一起也就是充血模型,而貧血領域模型則更像是一種面向過程設計,很多人認爲這些貧血領域對象是真正的對象,從而徹底誤解了面向對象設計的涵義。

貧血領域模型的根本問題是,它引入了領域模型設計的所有成本,卻沒有帶來任何好處。最主要的成本是將對象映射到數據庫中,從而產生了一個 O/R(對象關係)映射層。只有當你充分使用了面向對象設計來組織複雜的業務邏輯後,這一成本才能夠被抵消。如果將所有行爲都寫入到 Service 對象,那最終你會得到一組事務處理腳本,從而錯過了領域模型帶來的好處。而且當業務足夠複雜時, 你將會得到一堆爆炸的事務處理腳本。

4. 貧血模型與充血模型案例驗證

員工貧血模型

@Data
public class Person {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private Integer age;
    /**
     * 生日
     */
    private Date birthday;
    /**
     * 當前狀態
     */
    private Stauts stauts;
}
public class PersonServiceImpl implements PersonService {
    public void sleep(Person person) {
        person.setStauts(SleepStatus.get());
    }
public void setAgeByBirth(Person person) {
        Date birthday = person.getBirthday();
        if (currentDate.before(birthday)) {
            throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");
        }
        int yearNow = cal.get(Calendar.YEAR);
        int dayBirth = bir.get(Calendar.DAY_OF_MONTH);
        /*大概計算, 忽略月份等,年齡是當前年減去出生年*/
        int age = yearNow - yearBirth;
        person.setAge(age);
    }
}
public class WorkServiceImpl implements WorkService{
    public void code(Person person) {
        person.setStauts(CodeStatus.get());
    }
}

這一段代碼就是貧血對象的處理過程,Person 類, 通過 PersonService、WorkingService 去控制 Person 的行爲,第一眼看起來像是沒什麼問題,但是真正去思考整個流程。WorkingService, PersonService 到底是什麼樣的存在?與真實世界邏輯相比, 過於抽象。基於貧血模型的傳統開發模式,將數據與業務邏輯分離,違反了 OOP 的封裝特性,實際上是一種面向過程的編程風格。但是,現在幾乎所有的 Web 項目,都是基於這種貧血模型的開發模式,甚至連 Java Spring 框架的官方 demo,都是按照這種開發模式來編寫的。

面向過程編程風格有種種弊端,比如,數據和操作分離之後,數據本身的操作就不受限制了。任何代碼都可以隨意修改數據。

員工充血模型

@Data
public class Person extends Entity {
    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private Integer age;
    /**
     * 生日
     */
    private Date birthday;
    /**
     * 當前狀態
     */
    private Stauts stauts;
    public void code() {
        this.setStauts(CodeStatus.get());
    }
    public void sleep() {
        this.setStauts(SleepStatus.get());
    } 
    public void setAgeByBirth() {
        Date birthday = this.getBirthday();
        Calendar currentDate = Calendar.getInstance();
        if (currentDate.before(birthday)) {
            throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");
        }
        int yearNow = currentDate.get(Calendar.YEAR);
        int yearBirth = birthday.getYear();
        /*粗略計算, 忽略月份等,年齡是當前年減去出生年*/
        int age = yearNow - yearBirth;
        this.setAge(age);
    }       
}

貧血模型和充血模型的區別

/**
 * 貧血模型
 */
public class Client {
    @Resource
    private PersonService personService;
    @Resource
    private WorkService workService;
    public void test() {
        Person person = new Person();
        personService.setAgeByBirth(person);
        workService.code(person);
        personService.sleep(person);
    }
}
/**
 * 充血模型
 */
public class Client {
    public void test() {
        Person person = new Person();
        person.setAgeByBirth();
        person.code();
        person.sleep();
    }
}

上面兩段代碼很明顯第二段的認知成本更低, 這在滿是 Service,Manage 的系統下更爲明顯,Person 的行爲交由自己去管理, 而不是交給各種 Service 去管理。

7.2 DDD 實現銀行轉賬案例

銀行轉賬事務腳本實現在事務腳本的實現中,關於在兩個賬號之間轉賬的領域業務邏輯都被寫在了 MoneyTransferService 的實現裏面了,而 Account 僅僅是 getters 和 setters 的數據結構,也就是我們說的貧血模型:

public class MoneyTransferServiceTransactionScriptImpl implements MoneyTransferService {
    private AccountDao accountDao;
    private BankingTransactionRepository bankingTransactionRepository;
    //...
    @Override
    public BankingTransaction transfer(
            String fromAccountId, String toAccountId, double amount) {
        Account fromAccount = accountDao.findById(fromAccountId);
        Account toAccount = accountDao.findById(toAccountId);
        //. . .
        double newBalance = fromAccount.getBalance() - amount;
        switch (fromAccount.getOverdraftPolicy()) {
            case NEVER:
                if (newBalance < 0) {
                    throw new DebitException("Insufficient funds");
                }
                break;
            case ALLOWED:
                if (newBalance < -limit) {
                    throw new DebitException(
                            "Overdraft limit (of " + limit + ") exceeded: " + newBalance);
                }
                break;
        }
        fromAccount.setBalance(newBalance);
        toAccount.setBalance(toAccount.getBalance() + amount);
        BankingTransaction moneyTransferTransaction =
                new MoneyTranferTransaction(fromAccountId, toAccountId, amount);
        bankingTransactionRepository.addTransaction(moneyTransferTransaction);
        return moneyTransferTransaction;
    }
}

上面的代碼大家看起來應該比較眼熟,因爲目前大部分系統都是這麼寫的。其實我們是有辦法做的更優雅的,這種優雅的方式就是領域建模,唯有掌握了這種優雅你才能實現從工程師嚮應用架構的轉型。同樣的業務邏輯,接下來就讓我們看一下用 DDD 是怎麼做的。銀行轉賬領域模型實現如果用 DDD 的方式實現,Account 實體除了賬號屬性之外,還包含了行爲和業務邏輯,比如 debit() 和 credit() 方法。

// @Entity
public class Account {
    // @Id
    private String id;
    private double balance;
    private OverdraftPolicy overdraftPolicy;
    //. . .
    public double balance() {
        return balance;
    }
    public void debit(double amount) {
        this.overdraftPolicy.preDebit(this, amount);
        this.balance = this.balance - amount;
        this.overdraftPolicy.postDebit(this, amount);
    }
    public void credit(double amount) {
        this.balance = this.balance + amount;
    }
}

而且透支策略 OverdraftPolicy 也不僅僅是一個 Enum 了,而是被抽象成包含了業務規則並採用了策略模式的對象.

public interface OverdraftPolicy {
    void preDebit(Account account, double amount);
    void postDebit(Account account, double amount);
}
public class NoOverdraftAllowed implements OverdraftPolicy {
    public void preDebit(Account account, double amount) {
        double newBalance = account.balance() - amount;
        if (newBalance < 0) {
            throw new DebitException("Insufficient funds");
        }
    }
    public void postDebit(Account account, double amount) {
    }
}
public class LimitedOverdraft implements OverdraftPolicy {
    private double limit;
    //...
    public void preDebit(Account account, double amount) {
        double newBalance = account.balance() - amount;
        if (newBalance < -limit) {
            throw new DebitException(
                    "Overdraft limit (of " + limit + ") exceeded: " + newBalance);
        }
    }
    public void postDebit(Account account, double amount) {
    }
}
//而Domain Service只需要調用Domain Entity對象完成業務邏輯即可。
public class MoneyTransferServiceDomainModelImpl implements MoneyTransferService {
    private AccountRepository accountRepository;
    private BankingTransactionRepository bankingTransactionRepository;
    //...
    @Override
    public BankingTransaction transfer(
            String fromAccountId, String toAccountId, double amount) {
        Account fromAccount = accountRepository.findById(fromAccountId);
        Account toAccount = accountRepository.findById(toAccountId);
        //. . .
        fromAccount.debit(amount);
        toAccount.credit(amount);
        BankingTransaction moneyTransferTransaction =
                new MoneyTranferTransaction(fromAccountId, toAccountId, amount);
        bankingTransactionRepository.addTransaction(moneyTransferTransaction);
        return moneyTransferTransaction;
    }
}

通過上面的 DDD 重構後,原來在事務腳本中的邏輯,被分散到 Domain Service,Domain Entity 和 OverdraftPolicy 三個滿足 SOLID 的對象中。

技術老男孩 分享技術路上的點滴,專注於後端技術,助力開發者成長,歡迎關注。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/88UrpR5oPbnXNgjpp0HsWA