參考來源: 使用DDD指導(dǎo)業(yè)務(wù)設(shè)計(jì)的一點(diǎn)思考 https://insights.thoughtworks.cn/ddd-business-design/ 后端開發(fā)實(shí)踐系列——領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)編碼實(shí)踐 https://insights.thoughtworks.cn/backend-development-ddd
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)指導(dǎo)業(yè)務(wù)設(shè)計(jì)
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)是什么
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)是 Eric Evans 提出的一種軟件設(shè)計(jì)方法和思想,主要解決業(yè)務(wù)系統(tǒng)的設(shè)計(jì)和建模。
理論發(fā)展過程
DDD(Domain-Driven Design,領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)),對(duì)應(yīng)中文版為《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》。
Implement Domain-Driven Design,簡稱IDDD,中文版《實(shí)現(xiàn)領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》。 3.《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)模式、原理與實(shí)踐》問世,簡稱PPPDDD。
IDDD的精華版DDDD(Domain-Driven Design Distilled),《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)精粹》。
模型和領(lǐng)域模型
模型是能夠表達(dá)系統(tǒng)業(yè)務(wù)邏輯和狀態(tài)的對(duì)象。 模型,用來反映事物某部分特征的物件,無論是實(shí)物還是虛擬的。 領(lǐng)域,指的特定行業(yè)或者場(chǎng)景下的業(yè)務(wù)邏輯。 DDD 中的模型是指反應(yīng) IT 系統(tǒng)的業(yè)務(wù)邏輯和狀態(tài)的對(duì)象,是從具體業(yè)務(wù)(領(lǐng)域)中提取出來的,因此又叫做領(lǐng)域模型。
我們可以吧系統(tǒng)復(fù)雜的問題分為兩類:
業(yè)務(wù)復(fù)雜度
技術(shù)復(fù)雜度 技術(shù)復(fù)雜度,軟件設(shè)計(jì)中和技術(shù)實(shí)現(xiàn)相關(guān)的問題,例如處理用戶輸入,持久化模型,處理網(wǎng)絡(luò)通信等。 業(yè)務(wù)復(fù)雜度,軟件設(shè)計(jì)中和業(yè)務(wù)邏輯相關(guān)的問題,例如為訂單添加商品,需要計(jì)算訂單總價(jià),應(yīng)用折扣規(guī)則等。
識(shí)別上下文的邊界是 DDD 中最難得一部分,同時(shí)上下文邊界是由業(yè)務(wù)變化動(dòng)態(tài)變化的,我們把識(shí)別出邊界的上下文叫做限界上下文(Bounded Context)。 使用上下文之后,帶來另外一個(gè)收獲。模型之間本質(zhì)上沒有多對(duì)多關(guān)系,如果有,說明存在一個(gè)隱含的成員關(guān)系,這個(gè)關(guān)系沒有被充分的分析出來,對(duì)后期的開發(fā)會(huì)造成非常大的困擾。
我們將那相關(guān)性極強(qiáng)的領(lǐng)域模型放到一起考慮,數(shù)據(jù)的一致性必須解決,同時(shí)生命周期也需要保持同步,我們把這個(gè)集合叫做聚合。
聚合中需要選擇一個(gè)代表負(fù)責(zé)和全局通信,類似于一個(gè)部門的接口人,這樣就能確保數(shù)據(jù)保持一致。我們把這個(gè)模型叫做聚合根。
相對(duì)于非聚合根的模型,我們叫做實(shí)體。

我們把沒有自己生命周期的模型,僅用來呈現(xiàn)多個(gè)字段的值的模型和對(duì)象,稱作為值對(duì)象。

使用領(lǐng)域模型指導(dǎo)程序設(shè)計(jì)
指導(dǎo)數(shù)據(jù)庫設(shè)計(jì)
使用領(lǐng)域模型建立數(shù)據(jù)庫的要點(diǎn):
留意多對(duì)多關(guān)系,并拆解成一對(duì)多關(guān)系
值對(duì)象和實(shí)體往往為一對(duì)一關(guān)系
使用 ORM 時(shí),聚合根和實(shí)體可以配置為級(jí)聯(lián)刪除和更新
禁止聚合根之間進(jìn)行關(guān)聯(lián)
指導(dǎo) API 設(shè)計(jì)
RESTful API 已經(jīng)變成了主流 API 設(shè)計(jì)方式,當(dāng)設(shè)計(jì)好領(lǐng)域?qū)ο蠛?,設(shè)計(jì) API 的難度大大降低。
使用聚合根作為 URI 的根路徑,使用實(shí)體作為子路徑。通過 ID 作為 Path 參數(shù)。
指導(dǎo)對(duì)象設(shè)計(jì)
領(lǐng)域模型解決業(yè)務(wù)復(fù)雜度的問題,領(lǐng)域模型只應(yīng)該被用作處理業(yè)務(wù)邏輯,存儲(chǔ)、業(yè)務(wù)表現(xiàn)都應(yīng)該和領(lǐng)域模型無關(guān)。 可以把這些 Plain Object 分為三類:
DTO,和交互相關(guān)或者和后端、第三方服務(wù)對(duì)接
Entity,數(shù)據(jù)庫表映射
Model,領(lǐng)域模型
指導(dǎo)代碼組織
代碼組織,通俗來說就是如何分包。 DDD 特別的抽離出一層叫做 application。這一層是 DDD 的精華,領(lǐng)域模型關(guān)心業(yè)務(wù)邏輯,但是不關(guān)心業(yè)務(wù)場(chǎng)景。
application 用來隔離業(yè)務(wù)場(chǎng)景,顯得非常重要。
領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD)編碼實(shí)踐
實(shí)現(xiàn)業(yè)務(wù)的3種常見方式
基于“Service + 貧血模型”的實(shí)現(xiàn) 這種方式當(dāng)前被很多軟件項(xiàng)目所采用,主要的特點(diǎn)是:存在一個(gè)貧血的“領(lǐng)域?qū)ο蟆?,業(yè)務(wù)邏輯通過一個(gè)Service類實(shí)現(xiàn),然后通過setter方法更新領(lǐng)域?qū)ο?,最后通過DAO(多數(shù)情況下可能使用諸如Hibernate之類的ORM框架)保存到數(shù)據(jù)庫中。 這種方式依然是一種面向過程的編程范式,違背了最基本的OO原則。另外的問題在于職責(zé)劃分模糊不清,使本應(yīng)該內(nèi)聚在Order中的業(yè)務(wù)邏輯泄露到了其他地方(OrderService),導(dǎo)致Order成為一個(gè)只是充當(dāng)數(shù)據(jù)容器的貧血模型(Anemic Model),而非真正意義上的領(lǐng)域模型。在項(xiàng)目持續(xù)演進(jìn)的過程中,這些業(yè)務(wù)邏輯會(huì)分散在不同的Service類中,最終的結(jié)果是代碼變得越來越難以理解進(jìn)而逐漸喪失擴(kuò)展能力。
基于事務(wù)腳本的實(shí)現(xiàn) 我們會(huì)發(fā)現(xiàn)領(lǐng)域?qū)ο?Order)存在的唯一目的其實(shí)是為了讓ORM這樣的工具能夠一次性地持久化,在不使用ORM的情況下,領(lǐng)域?qū)ο笊踔炼紱]有必要存在。于是,此時(shí)的代碼實(shí)現(xiàn)便退化成了事務(wù)腳本(Transaction Script),也就是直接將Service類中計(jì)算出的結(jié)果直接保存到數(shù)據(jù)庫(或者有時(shí)都沒有Service類,直接通過SQL實(shí)現(xiàn)業(yè)務(wù)邏輯)。 在系統(tǒng)足夠簡單的情況下完全可以采用。但是:一方面“簡單”這個(gè)度其實(shí)并不容易把握;另一方面軟件系統(tǒng)通常會(huì)在不斷的演進(jìn)中加入更多的功能,使得原本簡單的代碼逐漸變得復(fù)雜。
基于領(lǐng)域?qū)ο蟮膶?shí)現(xiàn) 在這種方式中,核心的業(yè)務(wù)邏輯被內(nèi)聚在行為飽滿的領(lǐng)域?qū)ο?Order)中,實(shí)現(xiàn)Order類如下:
public void changeProductCount(ProductId productId, int count) {
if (this.status == PAID) {
throw new OrderCannotBeModifiedException(this.id);
}
OrderItem orderItem = retrieveItem(productId);
orderItem.updateCount(count);
}
然后在Controller或者Service中調(diào)用:
@PostMapping("/order/{id}/products")
public void changeProductCount(@PathVariable(name = "id") String id, @RequestBody @Valid ChangeProductCountCommand command) {
Order order = DAO.byId(orderId(id));
order.changeProductCount(ProductId.productId(command.getProductId()), command.getCount());
order.updateTotalPrice();
DAO.saveOrUpdate(order);
}
可以看到,所有業(yè)務(wù)(“檢查Order狀態(tài)”、“修改Product數(shù)量”以及“更新Order總價(jià)”)都被包含在了Order對(duì)象中,這些正是Order應(yīng)該具有的職責(zé)。