使用DDD指導(dǎo)業(yè)務(wù)設(shè)計(jì)的一點(diǎn)思考

領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)(DDD) 是 Eric Evans 提出的一種軟件設(shè)計(jì)方法和思想,主要解決業(yè)務(wù)系統(tǒng)的設(shè)計(jì)和建模。DDD 有大量難以理解的概念,尤其是翻譯的原因,某些詞匯非常生澀,例如:模型、限界上下文、聚合、實(shí)體、值對(duì)象等。

實(shí)際上 DDD 的概念和邏輯本身并不復(fù)雜,很多概念和名詞是為了解決一些特定的問題才引入的,并和面向?qū)ο笏枷爰嫒?,可以說 DDD 也是面向?qū)ο笏枷胫械囊粋€(gè)子集。如果遵從奧卡姆剃刀的原則,“如無必要,勿增實(shí)體”,我們先把 DDD 這些概念丟開,從一個(gè)案例出發(fā),在必要的時(shí)候?qū)⑦@些概念引入。

從紙和筆思考 IT 系統(tǒng)的工作邏輯

讓我真正對(duì)計(jì)算機(jī)軟件和建模有了更深入的認(rèn)識(shí)是在一家餐廳吃飯的時(shí)候。數(shù)年以前,我還在一家創(chuàng)業(yè)公司負(fù)責(zé)餐飲軟件的服務(wù)器端的開發(fā)工作,因?yàn)楣ぷ鞯脑?,外出就餐時(shí)常都會(huì)對(duì)餐廳的點(diǎn)餐系統(tǒng)仔細(xì)觀察,以便于改進(jìn)我們自己產(chǎn)品的設(shè)計(jì)。

一次偶然的情況,我們就餐的餐廳停電了,所幸是在白天,對(duì)我們的就餐并沒有什么影響。我突然很好奇這家店,在收銀系統(tǒng)無法工作的情況下怎么讓業(yè)務(wù)繼續(xù)運(yùn)轉(zhuǎn),因此我饒有趣味的等待服務(wù)員來接受我們的點(diǎn)單。

故事的發(fā)展并沒有超出預(yù)期,服務(wù)員拿了紙和筆,順利的完成了點(diǎn)餐,并將復(fù)寫紙復(fù)寫的底單麻溜的撕下來交給了后廚。我這時(shí)候才回過神來。

軟件工程師并沒有創(chuàng)造新的東西,只不過是數(shù)字世界的磚瓦工,計(jì)算機(jī)系統(tǒng)中合乎邏輯的過程,停電后人肉使用紙和筆一樣合乎邏輯。

合乎現(xiàn)實(shí)世界的邏輯和和規(guī)則,使用鼠標(biāo)和鍵盤代替紙和筆,就是軟件設(shè)計(jì)的基本邏輯。如果我們只是關(guān)注于對(duì)數(shù)據(jù)庫的增、刪、改、查(CRUD),實(shí)際上沒有對(duì)業(yè)務(wù)進(jìn)行正確的識(shí)別,這是導(dǎo)致代碼組織混亂的根本原因。

會(huì)計(jì)、餐飲、購物、人員管理、倉儲(chǔ),這些都是各個(gè)領(lǐng)域?qū)崒?shí)在在發(fā)生的事情,分析業(yè)務(wù)邏輯,從中找出固定的模式,抽象成計(jì)算機(jī)系統(tǒng)中對(duì)象并存儲(chǔ)。這就是 DDD 和面向?qū)ο笏枷胫熊浖_發(fā)的一般過程。

你可能會(huì)想,我們平時(shí)不就是這樣做的嗎?

現(xiàn)實(shí)是,我們往往馬上關(guān)注到數(shù)據(jù)庫的設(shè)計(jì)上,想當(dāng)然的設(shè)計(jì)出一些數(shù)據(jù)庫表,然后著手于界面、網(wǎng)絡(luò)請(qǐng)求、如何操作數(shù)據(jù)庫上,業(yè)務(wù)邏輯被封裝到一個(gè)叫做 Service 對(duì)象上,這個(gè)對(duì)象不承載任何狀態(tài),業(yè)務(wù)邏輯通過修改數(shù)據(jù)庫實(shí)現(xiàn)。

(一個(gè)樸素的應(yīng)用系統(tǒng))

一般來說這種方法也沒有大的問題,甚至工作的很好,F(xiàn)owler 將這種方法稱作為事務(wù)腳本(Transaction Script)。還有其他的設(shè)計(jì)模式,將用戶界面、業(yè)務(wù)邏輯、數(shù)據(jù)存儲(chǔ)作為一個(gè)“模塊”,可以實(shí)現(xiàn)用戶拖拽就可以實(shí)現(xiàn)簡單的編程,.net、VF曾經(jīng)提供過這種設(shè)計(jì)模式,這種設(shè)計(jì)模式叫做 SMART UI。

這種模式有一些好處。

  • 非常直觀,開發(fā)人員學(xué)習(xí)完編程基礎(chǔ)知識(shí)和數(shù)據(jù)庫 CRUD 操作之后就可以開發(fā)
  • 效率高,能短時(shí)間完成應(yīng)用開發(fā)
  • 模塊之間非常獨(dú)立

麻煩在于,當(dāng)業(yè)務(wù)復(fù)雜后,這種模式會(huì)帶來一些問題。

雖然最終都是對(duì)數(shù)據(jù)庫的修改,但是中間存在大量的業(yè)務(wù)邏輯,并沒有得到良好的封裝。客人退菜,并不是將訂單中的菜品移除這么簡單。需要將訂單的總額重新計(jì)算,以及需要通知后廚嘗試撤回正在制作中的菜。

不長眼的新手程序員擅自修改數(shù)據(jù)片段,整體業(yè)務(wù)邏輯被破壞。這是因?yàn)椴]有真正的一個(gè) “訂單” 的對(duì)象負(fù)責(zé)執(zhí)行相關(guān)的業(yè)務(wù)邏輯,Sevice 上的一個(gè)方法直接就對(duì)數(shù)據(jù)庫修改了,保持業(yè)務(wù)邏輯的完整,完全憑程序員對(duì)系統(tǒng)的了解。

(簡單的增刪改查帶來業(yè)務(wù)邏輯問題)

我們?cè)诟鱾€(gè)餐廳交流的時(shí)候,發(fā)現(xiàn)這并不是一個(gè) IT 系統(tǒng)的問題。某些管理不良餐廳,所有的服務(wù)員都可以收銀,而不是專門的收營員收銀;收營員劃掉菜品沒有更新小計(jì),另外的服務(wù)員結(jié)賬時(shí)會(huì)發(fā)生錯(cuò)誤。按照程序設(shè)計(jì)的語言來說,這些餐廳人員職責(zé)不清晰,不符合面向?qū)ο蟮囊恍┰瓌t。

我們吸收到這些業(yè)務(wù)邏輯到 IT 系統(tǒng)中來,并意識(shí)到系統(tǒng)中這里有一些隱藏的模型:

  • 訂單
  • 菜品

我們決定,抽象出訂單、菜品的對(duì)象,菜品不應(yīng)該被直接修改,而是通過訂單才能修改,無論任何情況,菜品的狀態(tài)變化都通過訂單來完成。

復(fù)雜系統(tǒng)的狀態(tài)被清晰的定義出來了, Service 承擔(dān)處理各個(gè)應(yīng)用場景的差異,模型對(duì)象處理一致的業(yè)務(wù)邏輯。

在接觸 Eric Evans 的 DDD 概念之前,我們沒有找到這種開發(fā)模式的名字,暫時(shí)稱作為 樸素模型驅(qū)動(dòng)開發(fā)。

(最基本的Repository模式)

模型和領(lǐng)域模型

從上面的例子中,模型是能夠表達(dá)系統(tǒng)業(yè)務(wù)邏輯和狀態(tài)的對(duì)象。

模型是一個(gè)非常寬泛的概念,任何東西都可以是模型,我們嘗試給模型下一個(gè)定義,并隨后繼續(xù)將領(lǐng)域模型的概念外延縮小。

模型,用來反映事物某部分特征的物件,無論是實(shí)物還是虛擬的古人用八個(gè)卦象作為世界運(yùn)行規(guī)律的模型;地圖用線條和顏色作為地理信息的模型;IT 系統(tǒng)用 E-R 作為對(duì)象或者數(shù)據(jù)庫表關(guān)系的模型;

我們知道要想做好一個(gè)可持續(xù)維護(hù)的 IT 系統(tǒng),實(shí)際上需要對(duì)業(yè)務(wù)進(jìn)行充分的抽象,找出這些隱藏的模型,并搬到系統(tǒng)中來。如果發(fā)生在餐廳的所有事物,都要能在系統(tǒng)中找到對(duì)應(yīng)的對(duì)象,那么這個(gè)系統(tǒng)的業(yè)務(wù)邏輯就非常完備。

現(xiàn)實(shí)世界中的業(yè)務(wù)邏輯,在 IT 系統(tǒng)業(yè)務(wù)分析時(shí),適合某個(gè)行業(yè)和領(lǐng)域相關(guān)的,所以又叫做領(lǐng)域。

領(lǐng)域,指的特定行業(yè)或者場景下的業(yè)務(wù)邏輯。

DDD 中的模型是指反應(yīng) IT 系統(tǒng)的業(yè)務(wù)邏輯和狀態(tài)的對(duì)象,是從具體業(yè)務(wù)(領(lǐng)域)中提取出來的,因此又叫做領(lǐng)域模型。

通過對(duì)實(shí)際業(yè)務(wù)出發(fā),而非馬上關(guān)注數(shù)據(jù)庫、程序設(shè)計(jì)。通過識(shí)別出固定的模式,并將這些業(yè)務(wù)邏輯的承載者抽象到一個(gè)模型上。這個(gè)模型負(fù)責(zé)處理業(yè)務(wù)邏輯,并表達(dá)當(dāng)前的系統(tǒng)狀態(tài)。這個(gè)過程就是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)。

我從這里面學(xué)到了什么呢?

我們做的計(jì)算機(jī)系統(tǒng)實(shí)際上,是替代了現(xiàn)實(shí)世界中的一些操作。按照面向?qū)ο笤O(shè)計(jì)的話,我們的系統(tǒng)是一個(gè)電子餐廳?,F(xiàn)實(shí)餐廳中的實(shí)體,應(yīng)該對(duì)應(yīng)到我們的系統(tǒng)中去,用于承載業(yè)務(wù),例如收銀員、顧客、廚師、餐桌、菜品,這些虛擬的實(shí)體表達(dá)了系統(tǒng)的狀態(tài),在某種程度上就能指代系統(tǒng),這就是模型,如果找到了這些元素,就很容易設(shè)計(jì)出軟件。

后來,如果我什么業(yè)務(wù)邏輯想不清楚,我就會(huì)把電斷掉,假裝自己是服務(wù)員,用紙和筆走一邊業(yè)務(wù)流程。

分析業(yè)務(wù),設(shè)計(jì)領(lǐng)域模型,編寫代碼。這就是領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)的基本過程。隨后會(huì)介紹,如何設(shè)計(jì)領(lǐng)域模型,當(dāng)我們建立了領(lǐng)域模型后,我可以考慮使用領(lǐng)域模型指導(dǎo)開發(fā)工作。

  • 指導(dǎo)數(shù)據(jù)庫設(shè)計(jì)
  • 指導(dǎo)模塊分包和代碼設(shè)計(jì)
  • 指導(dǎo) RESTful API 設(shè)計(jì)
  • 指導(dǎo)事務(wù)策略
  • 指導(dǎo)權(quán)限
  • 指導(dǎo)微服務(wù)劃分(有必要的情況)

(使用DDD的一般模式)

在我們之前的例子中,收銀員需要負(fù)責(zé)處理收銀的操作,同時(shí)表達(dá)這個(gè)餐廳有收營員這樣的一個(gè)狀態(tài)。收營員收到錢并記錄到賬本中,賬本負(fù)責(zé)處理記錄錢的業(yè)務(wù)邏輯,同時(shí)表達(dá)系統(tǒng)中有多少錢的狀態(tài)。

分析領(lǐng)域模型時(shí),請(qǐng)把”電“斷掉

我們進(jìn)行業(yè)務(wù)系統(tǒng)開發(fā)時(shí),大多數(shù)人都會(huì)認(rèn)同一個(gè)觀點(diǎn):將業(yè)務(wù)和模型設(shè)計(jì)清楚之后,開發(fā)起來會(huì)容易很多。

但是實(shí)際開發(fā)過程中,我們既要分析業(yè)務(wù),也要處理一些技術(shù)細(xì)節(jié),例如:如何響應(yīng)表單提交、如何存儲(chǔ)到數(shù)據(jù)庫、事務(wù)該怎么處理等。

使用領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)還有一個(gè)好處,我們可以通過隔離這些技術(shù)細(xì)節(jié),先進(jìn)行業(yè)務(wù)邏輯建模,然后再完成技術(shù)實(shí)現(xiàn),因?yàn)闃I(yè)務(wù)模型已經(jīng)建立,技術(shù)細(xì)節(jié)無非就是響應(yīng)用戶操作和持久化模型。

我們可以吧系統(tǒng)復(fù)雜的問題分為兩類:

  • 業(yè)務(wù)復(fù)雜度
  • 技術(shù)復(fù)雜度

(分離技術(shù)復(fù)雜度和業(yè)務(wù)復(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ī)則等。

當(dāng)我們分析業(yè)務(wù)并建模時(shí),過于關(guān)注技術(shù)實(shí)現(xiàn),會(huì)帶來極大的干擾。我學(xué)到最實(shí)用的思維方法,就是在這個(gè)過程把”電“斷掉,技術(shù)復(fù)雜度中的用戶交互想象成人工交談,持久化想象成用紙和筆記錄。

DDD 還強(qiáng)調(diào),業(yè)務(wù)建模應(yīng)該充分的和業(yè)務(wù)專家在一起,不應(yīng)該只是實(shí)現(xiàn)軟件的工程師自嗨。業(yè)務(wù)專家是一個(gè)虛擬的角色,有可能是一線業(yè)務(wù)人員、項(xiàng)目經(jīng)理、或者軟件工程師。

由于和業(yè)務(wù)專家一起完成建模,因此盡量不要選用非常專業(yè)的繪圖的工具和使用技術(shù)語言。 DDD 只是一種建模思想,并沒有規(guī)定使用的具體工具。我這里使用 PPT 的線條和形狀,用 E-R 的方式表達(dá)領(lǐng)域模型,如果大家都很熟悉 UML 也是可以的。甚至實(shí)際工作中,我們大量使用便利貼和白板完成建模工作。

這個(gè)建模過程可以是技術(shù)人員和業(yè)務(wù)專家一起討論出來,也可以是使用 ”事件風(fēng)暴“ 這類工作坊的方式完成。

這個(gè)過程非常重要,DDD 把這個(gè)過程稱作 協(xié)作設(shè)計(jì)。

通過這個(gè)過程,我們得到了領(lǐng)域模型。

(原始領(lǐng)域模型)

上圖使我們通過業(yè)務(wù)分析得到的一個(gè)非?;镜念I(lǐng)域模型,我們的點(diǎn)餐系統(tǒng)中,會(huì)有座位、訂單、菜品、評(píng)價(jià) 幾個(gè)模型。一個(gè)座位可以由多個(gè)訂單,每個(gè)訂單可以有多個(gè)菜品和評(píng)價(jià)。

同時(shí),菜品也會(huì)被不同的訂單使用。

上下文、二義性、統(tǒng)一語言

我們用這個(gè)模型開發(fā)系統(tǒng),使用領(lǐng)域模型驅(qū)動(dòng)的方式開發(fā),相對(duì)于事務(wù)腳本的方式,已經(jīng)容易和清晰很多了,但還是有一些問題。

有一天,市場告訴我們,這個(gè)系統(tǒng)會(huì)有一個(gè)邏輯問題。就是系統(tǒng)中菜品被刪除,訂單也不能查看。在我們之前的認(rèn)知里面,訂單和菜品是一個(gè)多對(duì)多的關(guān)系,菜品都不存在了,這個(gè)訂單還有什么用。

菜品,在這里存在了致命的二義性?。?!這里的菜品實(shí)際上有兩個(gè)含義:

  • 在訂單中,表達(dá)這個(gè)消費(fèi)項(xiàng)的記錄,也就是訂單項(xiàng)。例如,5號(hào)桌消費(fèi)的魚香肉絲一份。
  • 在菜品管理中,價(jià)格為30元的魚香肉絲,包含菜單圖片、文字描述,以及折扣信息。

菜品管理中的菜品下架后,不應(yīng)該產(chǎn)生新的訂單,同時(shí)也不應(yīng)該對(duì)訂單中的菜品造成任何影響。

這些問題是因?yàn)椋夹g(shù)專家和業(yè)務(wù)專家的語言沒有統(tǒng)一, DDD 認(rèn)識(shí)到了這個(gè)問題,統(tǒng)一語言是實(shí)現(xiàn)良好的領(lǐng)域模型的前提,因此應(yīng)該 ”大聲的建?!啊N以趨⑴c這個(gè)過程目睹過大量有意義的爭吵,正是這些爭吵讓領(lǐng)域模型變得原來越清晰。

這個(gè)過程叫做統(tǒng)一語言。

(領(lǐng)域模型v2)

和現(xiàn)實(shí)生活中一樣,產(chǎn)生二義性的原因是因?yàn)槲覀兊膶?duì)話發(fā)生在不同的上下文中,我們?cè)谡勔粋€(gè)概念必須在確定的上下文中才有意義。在不同的場景下,即使使用的詞匯相同,但是業(yè)務(wù)邏輯本質(zhì)都是不同的。想象一下,發(fā)生在《武林外傳》中同??蜅5膸锥螌?duì)話。

(對(duì)話)

這段對(duì)話中實(shí)際上有三個(gè)上下文,這里的 ”菜“ 這個(gè)詞出現(xiàn)了三次,但是實(shí)際上業(yè)務(wù)含義完全不同。

  • 大嘴說去買菜,這里的菜被抽象出來應(yīng)該是食材采購品,如果掌柜對(duì)這個(gè)菜進(jìn)行管理,應(yīng)該具有采購者、名稱、采購商家、采購價(jià)等。
  • 秀才說實(shí)習(xí)生把賬單中的菜算錯(cuò)了價(jià)格,秀才需要對(duì)賬單進(jìn)行管理,這里的菜應(yīng)該指的賬單科目,現(xiàn)實(shí)中一般是會(huì)計(jì)科目。
  • 老白說的客人點(diǎn)了一個(gè)醬鴨,這里老白關(guān)注的是訂單下面的訂單項(xiàng),訂單項(xiàng)包含的屬性有價(jià)格、數(shù)量、小計(jì)、折扣等信息。

實(shí)際上,還有一個(gè)隱藏的模型——上架中商品。掌柜需要添加菜品到菜單中,客人才能點(diǎn),這個(gè)商品就是我們平時(shí)一般概念上的商品。

我們把語言再次統(tǒng)一,得到新的模型。

(領(lǐng)域模型v3)

4個(gè)被紅色虛線框起來的區(qū)域中,我們都可以使用 ”菜品“ 這個(gè)詞匯(盡量不要這么做),但大家都明確 ”菜品“ 具有不同的含義。這個(gè)區(qū)域被叫做上下文。當(dāng)然上下文不只是由二義性決定的,還有可能是完全不相干的概念產(chǎn)生,例如訂單和座位實(shí)際概念上并沒有強(qiáng)烈的關(guān)聯(lián)關(guān)系,我們?cè)谡勛坏臅r(shí)候完全在談別的東西,所以座位也應(yīng)該是單獨(dú)的上下文。

識(shí)別上下文的邊界是 DDD 中最難得一部分,同時(shí)上下文邊界是由業(yè)務(wù)變化動(dòng)態(tài)變化的,我們把識(shí)別出邊界的上下文叫做限界上下文(Bounded Context)。限界上下文是一個(gè)非常有用的工具,限界上下文可以幫助我們識(shí)別出業(yè)務(wù)的邊界,并做適當(dāng)?shù)牟鸱帧?/p>

限界上下文的識(shí)別難以有一個(gè)明確的準(zhǔn)則,上下文的邊界非常模糊,需要有經(jīng)驗(yàn)的工程師并充分討論才能得到一個(gè)好的設(shè)計(jì)。同時(shí)需要注意,限界上下文的劃分沒有對(duì)錯(cuò),只有是否合適。跨限界上下文之間模型的關(guān)聯(lián)有本質(zhì)的不同,我們用虛線標(biāo)出,后面會(huì)聊到這種區(qū)別。

(領(lǐng)域模型v4)

使用上下文之后,帶來另外一個(gè)收獲。模型之間本質(zhì)上沒有多對(duì)多關(guān)系,如果有,說明存在一個(gè)隱含的成員關(guān)系,這個(gè)關(guān)系沒有被充分的分析出來,對(duì)后期的開發(fā)會(huì)造成非常大的困擾。

聚合根、實(shí)體、值對(duì)象

上面的模型,尤其是解決二義性這個(gè)問題之后,已經(jīng)能在實(shí)際開發(fā)中很好地使用了。不過還是會(huì)有一些問題沒有解決,實(shí)際開發(fā)中,每種模型的身份可能不太一樣,訂單項(xiàng)必須依賴訂單的存在而存在,如果能在領(lǐng)域模型圖中體現(xiàn)出來就更好了。

舉個(gè)例子來說,當(dāng)我們刪除訂單時(shí)候,訂單項(xiàng)應(yīng)該一起刪除,訂單項(xiàng)的存在必須依賴于訂單的存在。這樣業(yè)務(wù)邏輯是一致的和完整的,游離的訂單項(xiàng)對(duì)我們來說沒有意義,除非有特殊的業(yè)務(wù)需求存在。

為了解決這個(gè)問題,對(duì)待模型就不再是一視同仁了。我們將那相關(guān)性極強(qiáng)的領(lǐng)域模型放到一起考慮,數(shù)據(jù)的一致性必須解決,同時(shí)生命周期也需要保持同步,我們把這個(gè)集合叫做聚合。

聚合中需要選擇一個(gè)代表負(fù)責(zé)和全局通信,類似于一個(gè)部門的接口人,這樣就能確保數(shù)據(jù)保持一致。我們把這個(gè)模型叫做聚合根。當(dāng)一個(gè)聚合業(yè)務(wù)足夠簡單時(shí),聚合有可能只有一個(gè)模型組成,這個(gè)模型就是聚合根,常見的就是配置、日志相關(guān)的。

相對(duì)于非聚合根的模型,我們叫做實(shí)體

(領(lǐng)域模型v5)

我們把這個(gè)圖完善一下,聚合之間也是用虛線鏈接,為聚合根標(biāo)上橙色。識(shí)別聚合根需要一些技巧。

  • 聚合根本質(zhì)上也是實(shí)體,同屬于領(lǐng)域模型,用于承載業(yè)務(wù)邏輯和系統(tǒng)狀態(tài)。
  • 實(shí)體的生命周期依附于聚合根,聚合根刪除實(shí)體應(yīng)該也需要被刪除,保持系統(tǒng)一致性,避免游離的臟數(shù)據(jù)。
  • 聚合根負(fù)責(zé)和其他聚合通信,因此聚合根往往具有一個(gè)全局唯一標(biāo)識(shí)。例如,訂單有訂單 ID 和訂單號(hào),訂單號(hào)為全局業(yè)務(wù)標(biāo)識(shí),訂單 ID 為聚合內(nèi)關(guān)聯(lián)使用。聚合外使用訂單號(hào)進(jìn)行關(guān)聯(lián)應(yīng)用。

還有一類特殊的模型,這類模型只負(fù)責(zé)承載多個(gè)值的用處。在我們飯店的例子中,如果需要對(duì)賬單支持多國貨幣,我們將純數(shù)字的 price 字段修為 Price 類型。

public Clsss Price(){
    private String unit;
    private BigDecimal value;

    public Price(String unit,BigDecimal value){
        this.unit = unit;
        this.value = value;
    }
}

價(jià)格這個(gè)模型,沒有自己的生命周期,一旦被創(chuàng)建出來就無須修改,因?yàn)樾薷木透淖兞诉@個(gè)值本身。所以我們會(huì)給這類的對(duì)象一個(gè)構(gòu)造方法,然后去除掉所有的 setter 方法。

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

值對(duì)象一開始不是特別好理解,但是理解之后會(huì)讓系統(tǒng)設(shè)計(jì)非常清晰?!钡刂贰笆且粋€(gè)顯著的值對(duì)象。當(dāng)訂單發(fā)貨后,地址中的某一個(gè)屬性不應(yīng)該被單獨(dú)修改,因?yàn)楸恍薷闹筮@個(gè)”地址“就不再是剛剛那個(gè)”地址“,判斷地址是否相同我們會(huì)使用它的具體值:省、市、地、街道等。

值對(duì)象是相對(duì)于實(shí)體而言的,對(duì)比如下。

(實(shí)體和值對(duì)象對(duì)比)

另外值得一提的是,一個(gè)模型被作為值對(duì)象還是實(shí)體看待不是一成不變的,某些情況下需要作為實(shí)體設(shè)計(jì),但是在另外的條件下卻最好作為值對(duì)象設(shè)計(jì)。

地址,在一個(gè)大型系統(tǒng)充滿了二義性。

  • 作為訂單中的收貨地址時(shí),無需進(jìn)行管理,只需要表達(dá)街道、門牌號(hào)等信息,應(yīng)該作為值對(duì)象設(shè)計(jì)。為了避免歧義,可以重新命名為收貨地址。
  • 作為系統(tǒng)地理位置信息管理的情況中具有自己的生命周期,應(yīng)該作為實(shí)體設(shè)計(jì),并重命名為系統(tǒng)地址。
  • 作為用戶添加的自定義地址,用戶可以根據(jù) ID 進(jìn)行管理,應(yīng)該作為實(shí)體,并重命名為用戶地址。

我們使用藍(lán)色區(qū)別實(shí)體和聚合根,更新后的模型圖如下:

(領(lǐng)域模型v6)

雖然我們使用 E-R 的方式描述模型和模型之間的關(guān)系,但是這個(gè)E-R圖使用了顏色、虛線,已經(jīng)和傳統(tǒng)的 E-R 圖大不相同,把這種圖暫時(shí)叫做CE-R圖(Classified Entity Relationship)。DDD沒有規(guī)定如何畫圖,你可以使用其他任何畫圖的方法表達(dá)領(lǐng)域模型。

使用領(lǐng)域模型指導(dǎo)程序設(shè)計(jì)

在了解到 DDD 之前,到底該用一對(duì)多和多對(duì)多關(guān)系?RESTful API 設(shè)計(jì)時(shí)到底應(yīng)該選哪一個(gè)對(duì)象作為資源地址,評(píng)價(jià)應(yīng)該放到訂單路徑下還是單獨(dú)出來?訂單刪除相關(guān)有多少對(duì)象應(yīng)該納入事務(wù)管理?

在沒有領(lǐng)域模型之前,這些大概率憑借經(jīng)驗(yàn)決定,當(dāng)我們把領(lǐng)域模型設(shè)計(jì)出來之后,領(lǐng)域模型可以幫助我們做出這些指導(dǎo)。領(lǐng)域模型不只是為編寫業(yè)務(wù)邏輯代碼使用,這樣對(duì)領(lǐng)域模型來說就太可惜了。

下面是領(lǐng)域模型指導(dǎo)軟件開發(fā)的一些方面,具體細(xì)節(jié)后面會(huì)再逐個(gè)討論。

指導(dǎo)數(shù)據(jù)庫設(shè)計(jì)

通過 CE-R 圖,我們明顯可以設(shè)計(jì)出數(shù)據(jù)庫了。不過還有一些細(xì)節(jié)需要注意。

首先,在之前的認(rèn)知里面,多對(duì)多關(guān)系是非常正常的。但是通過對(duì)領(lǐng)域模型的分析后發(fā)現(xiàn),傳統(tǒng)處理多對(duì)多關(guān)系時(shí),需要額外增加一張關(guān)聯(lián)表,這張關(guān)聯(lián)表本質(zhì)上是一個(gè)”關(guān)系“的實(shí)體沒有被發(fā)掘出來。否則,在實(shí)際開發(fā)中會(huì)造成系統(tǒng)耦合,以及使用 ORM 的時(shí)候產(chǎn)生困惑。

菜品和訂單之間是多對(duì)多關(guān)系嗎?

如果是,菜品和訂單之間耦合了。實(shí)際上,菜品的管理處于系統(tǒng)操作的上游,菜品不依賴訂單的任何操作,也就是說訂單的任何變化菜品無需關(guān)心。

訂單擁有多個(gè)訂單項(xiàng),每個(gè)訂單項(xiàng)從菜品讀入數(shù)據(jù)并拷貝,或者引用一個(gè)菜品的全局 ID (菜品在另外一個(gè)聚合)。這樣在設(shè)計(jì)表結(jié)構(gòu)時(shí)訂單和訂單項(xiàng)關(guān)聯(lián),訂單項(xiàng)不關(guān)聯(lián)菜品。訂單項(xiàng)應(yīng)該從程序讀取菜品信息??雌饋矶鄬?duì)多的關(guān)系,被細(xì)致分析后,變成了一個(gè)一對(duì)多關(guān)系。

(數(shù)據(jù)庫設(shè)計(jì))

在使用 ORM 時(shí),良好的領(lǐng)域模型尤其有用。不合適的關(guān)聯(lián)關(guān)系不僅讓 ORM 關(guān)聯(lián)變得混亂,還會(huì)讓 ORM 的性能變差。

使用領(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ù)。

(API設(shè)計(jì))

值對(duì)象沒有 ID,應(yīng)該只能依附于某個(gè)實(shí)體的路徑下做更新操作。

(API設(shè)計(jì)v2)

另外根據(jù)這個(gè)關(guān)系,處理批量操作的時(shí)候應(yīng)該在實(shí)體的上一級(jí)完成,例如批量添加訂單的訂單項(xiàng),可以設(shè)計(jì)為:

POST /orders/{orderId}/items-batch

不要設(shè)計(jì)為:

POST /orders/{orderId}/items/batch

指導(dǎo)對(duì)象設(shè)計(jì)

在實(shí)踐中過程中,像 Java、Typescript具有類型系統(tǒng)的語言,對(duì)象很容易被誤用。如果 User 對(duì)象既被拿來當(dāng)做數(shù)據(jù)庫操作使用,又被拿來當(dāng)做接口呈現(xiàn)使用,這個(gè)類最終變成了上帝類,存在大量可有可無的屬性。

例如用戶注冊(cè)時(shí)候需要輸入重復(fù)密碼,如果在 User 對(duì)象中添加 confirmPassword 屬性,存儲(chǔ)時(shí)候確并不需要。

因此 DDD 中,數(shù)據(jù)庫各種對(duì)象的使用應(yīng)該針對(duì)不同的場景設(shè)計(jì)?;氐轿覀兩厦嬲f的技術(shù)復(fù)雜度和業(yè)務(wù)復(fù)雜度中來。領(lǐng)域模型解決業(yè)務(wù)復(fù)雜度的問題,領(lǐng)域模型只應(yīng)該被用作處理業(yè)務(wù)邏輯,存儲(chǔ)、業(yè)務(wù)表現(xiàn)都應(yīng)該和領(lǐng)域模型無關(guān)。

(對(duì)象設(shè)計(jì))

簡單來說,可以把這些 Plain Object 分為三類:

  • DTO,和交互相關(guān)或者和后端、第三方服務(wù)對(duì)接
  • Entity,數(shù)據(jù)庫表映射
  • Model,領(lǐng)域模型

另外,在使用領(lǐng)域模型使用上也需要額外注意

  • 領(lǐng)域?qū)ο蟊M量使用組合的方式,而不是繼承,現(xiàn)實(shí)業(yè)務(wù)邏輯中繼承這種概念實(shí)際上很少。例如菜品的設(shè)計(jì),有熱菜、湯菜、涼菜,實(shí)際上這里面并不是菜的繼承,而應(yīng)該抽象出分類這個(gè)模型。
  • 不要濫用領(lǐng)域模型,有些業(yè)務(wù)邏輯,實(shí)在找不出一個(gè)領(lǐng)域模型很正常,所以 DDD 中存在一個(gè)領(lǐng)域服務(wù)。例如,生成一個(gè) UUID。有些業(yè)務(wù)邏輯不持有系統(tǒng)業(yè)務(wù)狀態(tài),Eric 的書中比喻為像加油站一樣的業(yè)務(wù)邏輯。

指導(dǎo)代碼組織

代碼組織,通俗來說就是如何分包。一種狹義的對(duì) DDD 的理解就是指按照 DDD 風(fēng)格進(jìn)行代碼組織,雖然 DDD 的內(nèi)容遠(yuǎn)不止于此。

在很長一段時(shí)間,我對(duì) DDD 分包策略陷入困惑,后來我明白到,討論 DDD 風(fēng)格的分包,必須將單體引用和微服務(wù)應(yīng)用分開考慮。

微服務(wù)應(yīng)用在邏輯上和解耦良好的單體應(yīng)用是一致的。

但是微服務(wù)是一種分布式架構(gòu),映射到單體應(yīng)用中,各個(gè)包分布到不同的服務(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ù)場景,顯得非常重要。舉個(gè)例子,用戶被添加到系統(tǒng)中,領(lǐng)域模型處理的是:

  1. 用戶被添加
  2. 授予基本權(quán)限
  3. 積分規(guī)則創(chuàng)建
  4. 賬戶創(chuàng)建(三戶模型,客戶、用戶、賬戶往往分開)
  5. 客戶資料錄入

但是,用戶被添加到系統(tǒng)中由多個(gè)應(yīng)用場景觸發(fā)。

  • 用戶被邀請(qǐng)注冊(cè)
  • 用戶自己注冊(cè)
  • 管理員添加用戶

application 需要隔離應(yīng)用場景,并組織調(diào)配領(lǐng)域服務(wù),才能使得領(lǐng)域服務(wù)真正被復(fù)用。因此 application 需要承擔(dān)事務(wù)管理、權(quán)限控制、數(shù)據(jù)校驗(yàn)和轉(zhuǎn)換等操作。當(dāng)領(lǐng)域服務(wù)被調(diào)用時(shí),應(yīng)該是純粹業(yè)務(wù)邏輯,并與場景無關(guān)。

如果我們將三層架構(gòu)和 DDD 架構(gòu)對(duì)比,DDD 架構(gòu)如右圖所示。

(三層架構(gòu)對(duì)比)

我們將 DDD 的代碼架構(gòu)展開,可以看到更為細(xì)節(jié)的內(nèi)容。 DDD 代碼實(shí)現(xiàn)上需要 Repository、Factory 等概念,但這些是可選的,我們?cè)诤竺婢唧w講代碼結(jié)構(gòu)的部分再闡述。

(單體DDD架構(gòu))

我們?cè)賮砜矗珼DD 的單體應(yīng)用架構(gòu)映射到微服務(wù)架構(gòu)下會(huì)是怎么樣的。

(單體到微服務(wù))

微服務(wù)必須考慮到不再是一個(gè)服務(wù),Domain 層被抽離出來作為 Domain Server 存在,Domain Server 不關(guān)心業(yè)務(wù)場景,因此不需要 application 層。Application Server 需要 Application 層,Domain 層由后端的 Domain Server 提供。

另外補(bǔ)充,一些 DDD 代碼組織的基本邏輯:

  • 隔離業(yè)務(wù)復(fù)雜度和技術(shù)復(fù)雜度
  • 使用接口隔離有必要的耦合和依賴倒置

更多精彩洞見,請(qǐng)關(guān)注:ThoughtWorks洞見

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,346評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,889評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,118評(píng)論 0 286
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,558評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,739評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,980評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,347評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,702評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容