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