現實(shí)世界中的業(yè)務(wù)邏輯,在 IT 系統業(yè)務(wù)分析時(shí),適合某個(gè)行業(yè)和領(lǐng)域相關(guān)的,所以又叫做領(lǐng)域。領(lǐng)域,指的特定行業(yè)或者場(chǎng)景下的業(yè)務(wù)邏輯。DDD 中的模型是指反應 IT 系統的業(yè)務(wù)邏輯和狀態(tài)的對象,是從具體業(yè)務(wù)(領(lǐng)域)中提取出來(lái)的,因此又叫做領(lǐng)域模型。通過(guò)對實(shí)際業(yè)務(wù)出發(fā),而非馬上關(guān)注數據庫、程序設計。通過(guò)識別出固定的模式,并將這些業(yè)務(wù)邏輯的承載者抽象到一個(gè)模型上。這個(gè)模型負責處理業(yè)務(wù)邏輯,并表達當前的系統狀態(tài)。這個(gè)過(guò)程就是領(lǐng)域驅動(dòng)設計。我們做的計算機系統實(shí)際上,是替代了現實(shí)世界中的一些操作。按照面向對象設計的話(huà),我們的系統是一個(gè)電子餐廳?,F實(shí)餐廳中的實(shí)體,應該對應到我們的系統中去,用于承載業(yè)務(wù),例如收銀員、顧客、廚師、餐桌、菜品,這些虛擬的實(shí)體表達了系統的狀態(tài),在某種程度上就能指代系統,這就是模型,如果找到了這些元素,就很容易設計出軟件。后來(lái),如果我什么業(yè)務(wù)邏輯想不清楚,我就會(huì )把電斷掉,假裝自己是服務(wù)員,用紙和筆走一邊業(yè)務(wù)流程。分析業(yè)務(wù),設計領(lǐng)域模型,編寫(xiě)代碼。這就是領(lǐng)域驅動(dòng)設計的基本過(guò)程。隨后會(huì )介紹,如何設計領(lǐng)域模型,當我們建立了領(lǐng)域模型后,我可以考慮使用領(lǐng)域模型指導開(kāi)發(fā)工作。指導數據庫設計
指導模塊分包和代碼設計
指導 RESTful API 設計
指導事務(wù)策略
指導權限
在我們之前的例子中,收銀員需要負責處理收銀的操作,同時(shí)表達這個(gè)餐廳有收營(yíng)員這樣的一個(gè)狀態(tài)。收營(yíng)員收到錢(qián)并記錄到賬本中,賬本負責處理記錄錢(qián)的業(yè)務(wù)邏輯,同時(shí)表達系統中有多少錢(qián)的狀態(tài)。
分析領(lǐng)域模型時(shí),請把”電“斷掉
我們進(jìn)行業(yè)務(wù)系統開(kāi)發(fā)時(shí),大多數人都會(huì )認同一個(gè)觀(guān)點(diǎn):將業(yè)務(wù)和模型設計清楚之后,開(kāi)發(fā)起來(lái)會(huì )容易很多。但是實(shí)際開(kāi)發(fā)過(guò)程中,我們既要分析業(yè)務(wù),也要處理一些技術(shù)細節,例如:如何響應表單提交、如何存儲到數據庫、事務(wù)該怎么處理等。使用領(lǐng)域驅動(dòng)設計還有一個(gè)好處,我們可以通過(guò)隔離這些技術(shù)細節,先進(jìn)行業(yè)務(wù)邏輯建模,然后再完成技術(shù)實(shí)現,因為業(yè)務(wù)模型已經(jīng)建立,技術(shù)細節無(wú)非就是響應用戶(hù)操作和持久化模型。我們可以吧系統復雜的問(wèn)題分為兩類(lèi):(分離技術(shù)復雜度和業(yè)務(wù)復雜度)
技術(shù)復雜度,軟件設計中和技術(shù)實(shí)現相關(guān)的問(wèn)題,例如處理用戶(hù)輸入,持久化模型,處理網(wǎng)絡(luò )通信等。業(yè)務(wù)復雜度,軟件設計中和業(yè)務(wù)邏輯相關(guān)的問(wèn)題,例如為訂單添加商品,需要計算訂單總價(jià),應用折扣規則等。當我們分析業(yè)務(wù)并建模時(shí),過(guò)于關(guān)注技術(shù)實(shí)現,會(huì )帶來(lái)極大的干擾。我學(xué)到最實(shí)用的思維方法,就是在這個(gè)過(guò)程把”電“斷掉,技術(shù)復雜度中的用戶(hù)交互想象成人工交談,持久化想象成用紙和筆記錄。DDD 還強調,業(yè)務(wù)建模應該充分的和業(yè)務(wù)專(zhuān)家在一起,不應該只是實(shí)現軟件的工程師自嗨。業(yè)務(wù)專(zhuān)家是一個(gè)虛擬的角色,有可能是一線(xiàn)業(yè)務(wù)人員、項目經(jīng)理、或者軟件工程師。由于和業(yè)務(wù)專(zhuān)家一起完成建模,因此盡量不要選用非常專(zhuān)業(yè)的繪圖的工具和使用技術(shù)語(yǔ)言。DDD 只是一種建模思想,并沒(méi)有規定使用的具體工具。我這里使用 PPT 的線(xiàn)條和形狀,用 E-R 的方式表達領(lǐng)域模型,如果大家都很熟悉 UML 也是可以的。甚至實(shí)際工作中,我們大量使用便利貼和白板完成建模工作。這個(gè)建模過(guò)程可以是技術(shù)人員和業(yè)務(wù)專(zhuān)家一起討論出來(lái),也可以是使用 ”事件風(fēng)暴“ 這類(lèi)工作坊的方式完成。這個(gè)過(guò)程非常重要,DDD 把這個(gè)過(guò)程稱(chēng)作 協(xié)作設計。通過(guò)這個(gè)過(guò)程,我們得到了領(lǐng)域模型。(原始領(lǐng)域模型)
上圖使我們通過(guò)業(yè)務(wù)分析得到的一個(gè)非?;镜念I(lǐng)域模型,我們的點(diǎn)餐系統中,會(huì )有座位、訂單、菜品、評價(jià) 幾個(gè)模型。一個(gè)座位可以由多個(gè)訂單,每個(gè)訂單可以有多個(gè)菜品和評價(jià)。同時(shí),菜品也會(huì )被不同的訂單使用。
上下文、二義性、統一語(yǔ)言
我們用這個(gè)模型開(kāi)發(fā)系統,使用領(lǐng)域模型驅動(dòng)的方式開(kāi)發(fā),相對于事務(wù)腳本的方式,已經(jīng)容易和清晰很多了,但還是有一些問(wèn)題。有一天,市場(chǎng)告訴我們,這個(gè)系統會(huì )有一個(gè)邏輯問(wèn)題。就是系統中菜品被刪除,訂單也不能查看。在我們之前的認知里面,訂單和菜品是一個(gè)多對多的關(guān)系,菜品都不存在了,這個(gè)訂單還有什么用。菜品,在這里存在了致命的二義性?。?!這里的菜品實(shí)際上有兩個(gè)含義:菜品管理中的菜品下架后,不應該產(chǎn)生新的訂單,同時(shí)也不應該對訂單中的菜品造成任何影響。這些問(wèn)題是因為,技術(shù)專(zhuān)家和業(yè)務(wù)專(zhuān)家的語(yǔ)言沒(méi)有統一, DDD 認識到了這個(gè)問(wèn)題,統一語(yǔ)言是實(shí)現良好的領(lǐng)域模型的前提,因此應該 ”大聲的建?!?。我在參與這個(gè)過(guò)程目睹過(guò)大量有意義的爭吵,正是這些爭吵讓領(lǐng)域模型變得原來(lái)越清晰。(領(lǐng)域模型v2)
和現實(shí)生活中一樣,產(chǎn)生二義性的原因是因為我們的對話(huà)發(fā)生在不同的上下文中,我們在談一個(gè)概念必須在確定的上下文中才有意義。在不同的場(chǎng)景下,即使使用的詞匯相同,但是業(yè)務(wù)邏輯本質(zhì)都是不同的。想象一下,發(fā)生在《武林外傳》中同??蜅5膸锥螌υ?huà)。(對話(huà))
這段對話(huà)中實(shí)際上有三個(gè)上下文,這里的 ”菜“ 這個(gè)詞出現了三次,但是實(shí)際上業(yè)務(wù)含義完全不同。實(shí)際上,還有一個(gè)隱藏的模型——上架中商品。掌柜需要添加菜品到菜單中,客人才能點(diǎn),這個(gè)商品就是我們平時(shí)一般概念上的商品。(領(lǐng)域模型v3)
4個(gè)被紅色虛線(xiàn)框起來(lái)的區域中,我們都可以使用 ”菜品“ 這個(gè)詞匯(盡量不要這么做),但大家都明確 ”菜品“ 具有不同的含義。這個(gè)區域被叫做上下文。當然上下文不只是由二義性決定的,還有可能是完全不相干的概念產(chǎn)生,例如訂單和座位實(shí)際概念上并沒(méi)有強烈的關(guān)聯(lián)關(guān)系,我們在談座位的時(shí)候完全在談別的東西,所以座位也應該是單獨的上下文。識別上下文的邊界是 DDD 中最難得一部分,同時(shí)上下文邊界是由業(yè)務(wù)變化動(dòng)態(tài)變化的,我們把識別出邊界的上下文叫做限界上下文(Bounded Context)。限界上下文是一個(gè)非常有用的工具,限界上下文可以幫助我們識別出業(yè)務(wù)的邊界,并做適當的拆分。限界上下文的識別難以有一個(gè)明確的準則,上下文的邊界非常模糊,需要有經(jīng)驗的工程師并充分討論才能得到一個(gè)好的設計。同時(shí)需要注意,限界上下文的劃分沒(méi)有對錯,只有是否合適??缦藿缟舷挛闹g模型的關(guān)聯(lián)有本質(zhì)的不同,我們用虛線(xiàn)標出,后面會(huì )聊到這種區別。(領(lǐng)域模型v4)
使用上下文之后,帶來(lái)另外一個(gè)收獲。模型之間本質(zhì)上沒(méi)有多對多關(guān)系,如果有,說(shuō)明存在一個(gè)隱含的成員關(guān)系,這個(gè)關(guān)系沒(méi)有被充分的分析出來(lái),對后期的開(kāi)發(fā)會(huì )造成非常大的困擾。
聚合根、實(shí)體、值對象
上面的模型,尤其是解決二義性這個(gè)問(wèn)題之后,已經(jīng)能在實(shí)際開(kāi)發(fā)中很好地使用了。不過(guò)還是會(huì )有一些問(wèn)題沒(méi)有解決,實(shí)際開(kāi)發(fā)中,每種模型的身份可能不太一樣,訂單項必須依賴(lài)訂單的存在而存在,如果能在領(lǐng)域模型圖中體現出來(lái)就更好了。舉個(gè)例子來(lái)說(shuō),當我們刪除訂單時(shí)候,訂單項應該一起刪除,訂單項的存在必須依賴(lài)于訂單的存在。這樣業(yè)務(wù)邏輯是一致的和完整的,游離的訂單項對我們來(lái)說(shuō)沒(méi)有意義,除非有特殊的業(yè)務(wù)需求存在。為了解決這個(gè)問(wèn)題,對待模型就不再是一視同仁了。我們將那相關(guān)性極強的領(lǐng)域模型放到一起考慮,數據的一致性必須解決,同時(shí)生命周期也需要保持同步,我們把這個(gè)集合叫做聚合。聚合中需要選擇一個(gè)代表負責和全局通信,類(lèi)似于一個(gè)部門(mén)的接口人,這樣就能確保數據保持一致。我們把這個(gè)模型叫做聚合根。當一個(gè)聚合業(yè)務(wù)足夠簡(jiǎn)單時(shí),聚合有可能只有一個(gè)模型組成,這個(gè)模型就是聚合根,常見(jiàn)的就是配置、日志相關(guān)的。(領(lǐng)域模型v5)
我們把這個(gè)圖完善一下,聚合之間也是用虛線(xiàn)鏈接,為聚合根標上橙色。識別聚合根需要一些技巧。- 聚合根本質(zhì)上也是實(shí)體,同屬于領(lǐng)域模型,用于承載業(yè)務(wù)邏輯和系統狀態(tài)。
- 實(shí)體的生命周期依附于聚合根,聚合根刪除實(shí)體應該也需要被刪除,保持系統一致性,避免游離的臟數據。
- 聚合根負責和其他聚合通信,因此聚合根往往具有一個(gè)全局唯一標識。例如,訂單有訂單 ID 和訂單號,訂單號為全局業(yè)務(wù)標識,訂單 ID 為聚合內關(guān)聯(lián)使用。聚合外使用訂單號進(jìn)行關(guān)聯(lián)應用。
還有一類(lèi)特殊的模型,這類(lèi)模型只負責承載多個(gè)值的用處。在我們飯店的例子中,如果需要對賬單支持多國貨幣,我們將純數字的 price 字段修為 Price 類(lèi)型。public Clsss Price(){
private String unit;
private BigDecimal value;
public Price(String unit,BigDecimal value){
this.unit = unit;
this.value = value;
}
}
價(jià)格這個(gè)模型,沒(méi)有自己的生命周期,一旦被創(chuàng )建出來(lái)就無(wú)須修改,因為修改就改變了這個(gè)值本身。所以我們會(huì )給這類(lèi)的對象一個(gè)構造方法,然后去除掉所有的 setter 方法。我們把沒(méi)有自己生命周期的模型,僅用來(lái)呈現多個(gè)字段的值的模型和對象,稱(chēng)作為值對象。值對象一開(kāi)始不是特別好理解,但是理解之后會(huì )讓系統設計非常清晰?!钡刂贰笆且粋€(gè)顯著(zhù)的值對象。當訂單發(fā)貨后,地址中的某一個(gè)屬性不應該被單獨修改,因為被修改之后這個(gè)”地址“就不再是剛剛那個(gè)”地址“,判斷地址是否相同我們會(huì )使用它的具體值:省、市、地、街道等。另外值得一提的是,一個(gè)模型被作為值對象還是實(shí)體看待不是一成不變的,某些情況下需要作為實(shí)體設計,但是在另外的條件下卻最好作為值對象設計。地址,在一個(gè)大型系統充滿(mǎn)了二義性。我們使用藍色區別實(shí)體和聚合根,更新后的模型圖如下:(領(lǐng)域模型v6)
雖然我們使用 E-R 的方式描述模型和模型之間的關(guān)系,但是這個(gè)E-R圖使用了顏色、虛線(xiàn),已經(jīng)和傳統的 E-R 圖大不相同,把這種圖暫時(shí)叫做CE-R圖(Classified Entity Relationship)。DDD沒(méi)有規定如何畫(huà)圖,你可以使用其他任何畫(huà)圖的方法表達領(lǐng)域模型。
使用領(lǐng)域模型指導程序設計
在了解到 DDD 之前,到底該用一對多和多對多關(guān)系?RESTful API 設計時(shí)到底應該選哪一個(gè)對象作為資源地址,評價(jià)應該放到訂單路徑下還是單獨出來(lái)?訂單刪除相關(guān)有多少對象應該納入事務(wù)管理?在沒(méi)有領(lǐng)域模型之前,這些大概率憑借經(jīng)驗決定,當我們把領(lǐng)域模型設計出來(lái)之后,領(lǐng)域模型可以幫助我們做出這些指導。領(lǐng)域模型不只是為編寫(xiě)業(yè)務(wù)邏輯代碼使用,這樣對領(lǐng)域模型來(lái)說(shuō)就太可惜了。下面是領(lǐng)域模型指導軟件開(kāi)發(fā)的一些方面,具體細節后面會(huì )再逐個(gè)討論。指導數據庫設計
通過(guò) CE-R 圖,我們明顯可以設計出數據庫了。不過(guò)還有一些細節需要注意。首先,在之前的認知里面,多對多關(guān)系是非常正常的。但是通過(guò)對領(lǐng)域模型的分析后發(fā)現,傳統處理多對多關(guān)系時(shí),需要額外增加一張關(guān)聯(lián)表,這張關(guān)聯(lián)表本質(zhì)上是一個(gè)”關(guān)系“的實(shí)體沒(méi)有被發(fā)掘出來(lái)。否則,在實(shí)際開(kāi)發(fā)中會(huì )造成系統耦合,以及使用 ORM 的時(shí)候產(chǎn)生困惑。如果是,菜品和訂單之間耦合了。實(shí)際上,菜品的管理處于系統操作的上游,菜品不依賴(lài)訂單的任何操作,也就是說(shuō)訂單的任何變化菜品無(wú)需關(guān)心。訂單擁有多個(gè)訂單項,每個(gè)訂單項從菜品讀入數據并拷貝,或者引用一個(gè)菜品的全局 ID (菜品在另外一個(gè)聚合)。這樣在設計表結構時(shí)訂單和訂單項關(guān)聯(lián),訂單項不關(guān)聯(lián)菜品。訂單項應該從程序讀取菜品信息??雌饋?lái)多對多的關(guān)系,被細致分析后,變成了一個(gè)一對多關(guān)系。(數據庫設計)
在使用 ORM 時(shí),良好的領(lǐng)域模型尤其有用。不合適的關(guān)聯(lián)關(guān)系不僅讓 ORM 關(guān)聯(lián)變得混亂,還會(huì )讓 ORM 的性能變差。使用領(lǐng)域模型建立數據庫的要點(diǎn):留意多對多關(guān)系,并拆解成一對多關(guān)系
值對象和實(shí)體往往為一對一關(guān)系
使用 ORM 時(shí),聚合根和實(shí)體可以配置為級聯(lián)刪除和更新
- 禁止聚合根之間進(jìn)行關(guān)聯(lián)
指導 API 設計
RESTful API 已經(jīng)變成了主流 API 設計方式,當設計好領(lǐng)域對象后,設計 API 的難度大大降低。使用聚合根作為 URI 的根路徑,使用實(shí)體作為子路徑。通過(guò) ID 作為 Path 參數。(API設計)
值對象沒(méi)有 ID,應該只能依附于某個(gè)實(shí)體的路徑下做更新操作。(API設計v2)
另外根據這個(gè)關(guān)系,處理批量操作的時(shí)候應該在實(shí)體的上一級完成,例如批量添加訂單的訂單項,可以設計為:POST /orders/{orderId}/items-batchPOST /orders/{orderId}/items/batch指導對象設計
在實(shí)踐中過(guò)程中,像 Java、Typescript具有類(lèi)型系統的語(yǔ)言,對象很容易被誤用。如果 User 對象既被拿來(lái)當做數據庫操作使用,又被拿來(lái)當做接口呈現使用,這個(gè)類(lèi)最終變成了上帝類(lèi),存在大量可有可無(wú)的屬性。例如用戶(hù)注冊時(shí)候需要輸入重復密碼,如果在 User 對象中添加 confirmPassword 屬性,存儲時(shí)候確并不需要。因此 DDD 中,數據庫各種對象的使用應該針對不同的場(chǎng)景設計?;氐轿覀兩厦嬲f(shuō)的技術(shù)復雜度和業(yè)務(wù)復雜度中來(lái)。領(lǐng)域模型解決業(yè)務(wù)復雜度的問(wèn)題,領(lǐng)域模型只應該被用作處理業(yè)務(wù)邏輯,存儲、業(yè)務(wù)表現都應該和領(lǐng)域模型無(wú)關(guān)。(對象設計)
簡(jiǎn)單來(lái)說(shuō),可以把這些 Plain Object 分為三類(lèi):另外,在使用領(lǐng)域模型使用上也需要額外注意指導代碼組織
代碼組織,通俗來(lái)說(shuō)就是如何分包。一種狹義的對 DDD 的理解就是指按照 DDD 風(fēng)格進(jìn)行代碼組織,雖然 DDD 的內容遠不止于此。在很長(cháng)一段時(shí)間,我對 DDD 分包策略陷入困惑,后來(lái)我明白到,討論 DDD 風(fēng)格的分包,必須將單體引用和微服務(wù)應用分開(kāi)考慮。微服務(wù)應用在邏輯上和解耦良好的單體應用是一致的。但是微服務(wù)是一種分布式架構,映射到單體應用中,各個(gè)包分布到不同的服務(wù)器中了。我們先以單體應用入手,最后再討論如何將單體應用架構映射到到微服務(wù)中。在事務(wù)腳本的模式中,我們一般將代碼分為三層架構。DDD 特別的抽離出一層叫做 application。這一層是 DDD 的精華,領(lǐng)域模型關(guān)心業(yè)務(wù)邏輯,但是不關(guān)心業(yè)務(wù)場(chǎng)景。application 用來(lái)隔離業(yè)務(wù)場(chǎng)景,顯得非常重要。舉個(gè)例子,用戶(hù)被添加到系統中,領(lǐng)域模型處理的是:
用戶(hù)被添加
授予基本權限
積分規則創(chuàng )建
賬戶(hù)創(chuàng )建(三戶(hù)模型,客戶(hù)、用戶(hù)、賬戶(hù)往往分開(kāi))
但是,用戶(hù)被添加到系統中由多個(gè)應用場(chǎng)景觸發(fā)。application 需要隔離應用場(chǎng)景,并組織調配領(lǐng)域服務(wù),才能使得領(lǐng)域服務(wù)真正被復用。因此 application 需要承擔事務(wù)管理、權限控制、數據校驗和轉換等操作。當領(lǐng)域服務(wù)被調用時(shí),應該是純粹業(yè)務(wù)邏輯,并與場(chǎng)景無(wú)關(guān)。如果我們將三層架構和 DDD 架構對比,DDD 架構如右圖所示。(三層架構對比)
我們將 DDD 的代碼架構展開(kāi),可以看到更為細節的內容。DDD 代碼實(shí)現上需要 Repository、Factory 等概念,但這些是可選的,我們在后面具體講代碼結構的部分再闡述。(單體DDD架構)
我們再來(lái)看,DDD 的單體應用架構映射到微服務(wù)架構下會(huì )是怎么樣的。(單體到微服務(wù))
微服務(wù)必須考慮到不再是一個(gè)服務(wù),Domain 層被抽離出來(lái)作為 Domain Server 存在,Domain Server 不關(guān)心業(yè)務(wù)場(chǎng)景,因此不需要 application 層。Application Server 需要 Application 層,Domain 層由后端的 Domain Server 提供。
本站僅提供存儲服務(wù),所有內容均由用戶(hù)發(fā)布,如發(fā)現有害或侵權內容,請
點(diǎn)擊舉報。