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