DDD落地指南-架構師眼中的餐廳

在去年、我整理了一篇名爲《如何做架構設計?》的文章,主要探討了架構設計的目標和過程,然而、那是一篇概括性的文章,用於啓發思路,並不是具體的實踐指南,因此、我一直期望給出具體參考案例。

我幾乎忘了這件事,如今回顧、我發現並沒有合適的案例可供參考,現有的案例要麼不完整、要麼是與業務耦合的特定場景,要麼無法支撐研發落地。所以我決定從實際生活中出發,虛擬一個案例場景,以便能夠系統性的闡述這個問題。



正文開始




本案例側重於DDD的實踐,從實際業務場景推導軟件架構,將業務元素映射爲系統元素,讓系統本身成爲最好的業務文檔。在本案例中,我們選擇餐廳作爲業務場景,但不在意餐廳實現細節,而是以餐廳爲主線故事,系統性的闡述DDD落地方法。希望讀者能夠從中吸取精華,去其糟粕,全文較長、耐心讀完、必有收穫。




1、領域設計

領域設計的核心是業務驅動的分而治之,旨在縮小軟件系統與真實業務的差異,從而減少差異帶來的問題。

當業務與系統之間存在差異時,我們無法將業務邏輯和程序邏輯對應起來,從而分不清區域,也分不清職責,因此會覺得混亂。就像你平時不會將枕頭和被子放在廚房或衛生間一樣,你的牀上不會放着大米白麪,否則你想睡覺是一件很複雜的事情,軟件系統也是如此。

所以、首先要把業務分析清楚,然後設計與業務模型對應的軟件模型,這就是DDD的核心思想。



1.1 宏觀流程

假如我要設計一個餐廳,由於分而治之的需要,我會首先從宏觀流程去分析,可以幫我們迅速找到重要的區域(這是功能相關性的初步劃分)。





 

因此會得到幾個明確的行爲區域,我將餐廳劃分爲“菜品域”,“訂單域”,“廚房域”,“用餐域”,這是宏觀級別的領域劃分,後續應該針對每個區域單獨分析。

產出物是:宏觀流程和參與角色



1.2 統一語言

語言貫穿於整個開發過程,從需求分析到設計、從設計到編碼,因此好的語言非常重要,好的語言體現了清晰的業務概念

在這個階段,我們需要通過梳理,找到業務中都有哪些實體與行爲,對其做一些歸納。我們的核心問題是“誰”通過什麼“行爲”影響了“誰”,其中的三個要素分別是“角色”、“行爲”、“實體”,因此我的建議是先找到 “角色”、“行爲”、“實體”,並對他們歸類,我常常關注角色以及具體身份、行爲以及包含的重要步驟、實體以及具體實例。

角色:是施事主語、是名詞,是主動發起行爲的一類實體。

行爲:是動詞、是做了什麼事情,是行爲本身。

實體:是名詞,是除“角色”之外的其他實體。

推薦使用腦圖畫出來,我認爲歸納後的腦圖有助於我們識別根本要素,有利於抽象。

產出物是:名詞、概念定義、相關腦圖。





 



1.3 用例分析

在這一步、我們使用相對宏觀的分析,不需要進入用例的細節分析,主要的目的是掌握角色與行爲之間的關係,理清誰在做什麼,角色的職責目的是什麼,用於指導領域劃分以及領域服務設計。

產出物:用例圖

以做菜爲例,如圖





 



1.4 領域劃分

我們在分析宏觀流程時,劃分了幾個行爲區域,那是宏觀級別的。在那基礎之上,我們需要拉進某個區域的視角,再結合之前的用例分析,按照“功能相關性”、“角色相關性”進一步劃分領域。我們不僅要知道誰做了什麼,還需要知道誰“在哪”做了什麼。

功能相關性:也稱爲業務相關性,業務是由一套用例組成的,一套用例之間是符合高內聚原則的,一套用例構成了一個問題空間,也就構成了一個領域,所以“功能相關性”是劃分領域的黃金標準。例如與做菜相關的用例都應該歸屬於廚房,所以我們確認了廚房域,這也是很自然的事。在這一步,通過劃分領域、梳理領域與用例之間的關係

角色相關性:角色相關性不可以作爲首要參考因素,在特殊情況下用於劃分子域,某個區域涉及多個角色參與,可以按照角色的分工,拆分爲多個子域,從而滿足不同角色的個性化需要。例如廚房的採購人員負責買菜、刀工負責切菜、大廚負責烹飪。我們就會考慮將廚房劃分爲“採購子域”、“加工子域”、“烹飪子域”。通常來說,子域不具備獨立的問題空間,不會作爲獨立的領域存在。

劃分領域的核心原則是保證領域的自治性(最小完備和自我履行),謹慎使用“實體相關性”劃分領域,否則有可能將一個功能打散在多個領域上,違反了自治性原則,如果按照功能相關性劃分,更容易實現領域的自治性,並且有助於將功能需要的實體聚合在一起。

產出物:領域、子域、領域與用例的關係

以廚房域爲例,如圖





 



在複雜業務時,可以使用事件風暴方法輔助分析,並輸出上述產出物。



1.5 領域服務

什麼是領域服務?一個領域可以有幾個領域服務? 我們如何劃分領域服務?標準是什麼?

我認爲一個領域不只有一個領域服務,我們不應該按照實體劃分,也不應該按照聚合劃分,也不該按照功能相關性劃分。

領域服務用於實現用例功能,我認爲應該使用角色劃分領域服務。在用例圖中,不同的角色發起不一樣的用例,不同的領域服務提供不一樣的用例,只有這樣、才能確保領域服務是用例圖的映射,也才能真正體現業務含義。領域服務是面向角色的,在一個領域中、每個角色對應一個領域服務。另外、同一個用例的邏輯差異是與角色的身份有關的,角色的身份對應了服務的泛化,角色的用例對應了服務的方法。對於此觀點、我們在後續功能設計的部分也有體現。

例如:廚房域(廚師服務、刀工服務、採購員服務),菜品域(客戶服務、管理者服務)。

產出物:領域服務類圖





 



1.6 領域建模

我們思考一下,到底什麼纔是領域驅動設計? 例如“廚房域”被稱爲“菜域”,“廚師”的“做菜”功能被稱爲“菜服務”的“做菜”功能,也例如“菜品域”有個“菜品服務”,“菜品服務”提供了“增、刪、改、查”的功能。我們往往以最核心的實體爲中心,誤以爲業務就是在操作數據,丟掉了業務本質含義,逐漸也就走歪了。

不要學傳統的數據模型驅動設計,實體模型驅動設計與前者的本質是一樣的,是換湯不換藥的,這不是技術問題,而是過度集中在實體上以至於忘記其他元素。我們必須把精力放在業務本身,防止領域驅動設計變成領域模型驅動設計。我們不應該優先思考領域模型,不應該以領域模型命名一切,不應該讓領域模型決定業務的實現方式。廚房不只有菜,也有服務員和廚師,我們使用合適的語言對應合適的元素,以確保軟件元素是真實業務的映射。例如“廚師在廚房做菜”,這句話中的所有元素都要在系統中得以保留,丟了一個也不行,更何況只剩下菜了。

所以、我們先做領域劃分,再做領域服務設計,最後做領域建模,這個順序很重要,可以避免我們錯誤的以領域建模爲中心。先有用例纔有領域,先有領域才爲領域建模,實體是爲了實現一組用例存在的。而一組用例不一定依賴實體。

回到正題、我們在這一步的重點還是菜的問題,我們分析實體與領域之間關係(領域聚合),實體與實體的關係(OO聚合)。其中OO關係影響了功能的擴展性,需要我們特別關注。實體因一套用例而聚合在一起,我推薦做法是將領域的用例放在一起分析,找到他們的共同性,充分考慮變化,使用兼容性更好的模型解決問題。





 



組合、聚合

聚合(aggregation):聚合關係是一種弱的關係,整體和部分可以相互獨立。

組合(composition):組合關係是一種強的整體和部分的關係,整體和部分具有相同的生命週期。

可以使用如下案例,既能表達領域聚合,又能表達OO聚合的關係。





 

產出物:聚合、實體、值對象、實體的屬性



1.7 領域上下游

領域上下游關係,不是領域的依賴關係,依賴關係指的是能力的依賴,是共用了某些能力。領域上下游關係,也不是調用關係,調用關係是與用例相關的,不是用於描述領域處境的。

領域上下游關係指的是影響力的關係,上游影響下游,影響力分爲“邏輯影響”和“數據影響”,一般說來我們更應該關注“數據影響”,因爲上下游的邏輯影響也是靠數據傳遞的,所以領域上下游關係是一種數據流向的限制,是業務發生的順序限制,用於規定該領域所使用的數據,是下游領域依賴上游領域“準備就緒”的體現。合理的上下游限制,有助於減少領域之間的不必要依賴和重複的計算。

領域上下游是與場景相關的,並不是一成不變的,不同場景的情況下,存在不同的上下游關係,各場景應該獨立說明。

產出物:各場景的上下游說明



例:在【菜品管理】場景下





 

如果廚房的某些食材不足了,或者某個廚師休假了,就會影響到菜品的展示,從而影響到客戶的訂單。



例:在【客戶消費】場景下





 

客戶的訂單、影響廚房生產的菜,從而影響刀工的行爲,也影響到了採購。



請對比下面兩個圖,用於理解領域的上下游





 

實際上,廚師不應該依賴採購人員的採購功能,也不依賴刀工的切菜功能,他只是依賴“初加工食材”而已,而“初加工食材”就是被處理好的數據,廚師在做飯時,“初加工食材”就已經被處理好了,上面的圖例只是爲了說明一個關於領域上下游的問題,這是業務發生順序以及數據來源的問題。

我們常常使用領域事件串聯業務流程,在使用領域事件時,不止要關注點對點的解耦,更應該使業務流程符合領域上下游限定,讓各個領域獨立運行

順序發生優於嵌套發生,數據依賴優於功能依賴。



2、架構設計

架構設計是爲了解決軟件系統複雜度帶來的問題,找到系統中的元素並搞清楚他們之間關係

架構的目標是用於管理複雜性、易變性和不確定性,以確保在長期的系統演化過程中,一部分架構的變化不會對其它部分產生不必要的負面影響。這樣做可以確保業務和研發效率的敏捷,讓應用的易變部分能夠頻繁地變化,對應用的其它部分的影響儘可能地小。

架構設計三原則:合適原則、簡單原則、演化原則

2.1 分層架構

我們需要按照 接口層、領域層(領域用例層、領域模型層)依賴層、基礎層 構建架構模型

接口層:爲外部提供服務的入口,是適配層的北向網關。不實現任何業務邏輯,也不處理事務,是跨領域的,是流程編排層,是門面服務。

領域用例層:是領域服務層,是領域用例的實現層、隸屬於某個領域、是業務邏輯層,是事務層,業務邏輯應該在這層完整體現,不要分散到其他層級。

領域模型層:是領域模型(實體、值對象、聚合)的所在位置,專注於領域模型自身的能力,不包含業務功能,可以處理事務,是原子化的能力,是領域對象的自我實現

依賴層: 是連接外部服務的出口,是適配層的南向網關。包括倉儲,端點、RPC等,主要作用是領域和外部解耦,是跨領域的。

基礎層:與業務無關的,與領域無關的,通用的技術能力,技術組件等。



2.2 架構映射

架構的視角,從大到小依次是:系統->應用(微服務)->模塊(包)->子模塊 這樣的從大到小的層級。

業務領域映射:我們將劃分好的領域,按照對應的視角映射爲對應的元素,領域模型映射到架構模型時,應該是視角對等的,如果餐廳是系統、那麼廚房就是應用,如果餐廳是應用、那麼廚房就是模塊。也應該是層級匹配的,將用例的實現映射到用例層,將領域模型的實現映射到領域模型層。也應該是名稱一致的,將領域名映射爲應用名或包名,將實體名映射爲實體類名,將角色名映射爲領域服務類名,將角色身份名映射爲服務類的子類名,將用例名映射爲服務類的方法名。

技術和抽象問題:有時候、業務領域分析不能體現那些共性的技術問題,所以需要適當結合技術視角,可能需要對領域模型微調。同時、我們需要找到共同需要的基礎能力,例如“水”、“電”、“煤氣”等等,將這些作爲額外的考慮因素,要做到業務問題與技術問題解耦,不要將技術問題和業務邏輯揉成一團

領域設計,類似餐廳設計師,他設計餐廳有幾個區域,區域的用途是什麼。

架構設計,類似建築設計師,他設計如何走水電煤氣、如何施工等。

產出物:分層架構圖



以廚房爲視角,其架構如下





 



以餐廳爲視角,其架構如下





 

分層架構圖,體現邏輯上的層級分佈,而不是代表組件的具體含義,組件是應用還是模塊、需要結合實際情況而定。



2.3 必要的約束

1、分層架構越往下層就越是穩定的下層是被上層依賴的,下層不可以反向依賴上層(擴展點除外)。因爲分層架構的核心原則是將容易變化的邏輯上浮,將共性的、原子化的、通用的邏輯下沉,被依賴的下層應該是穩定的,這要求上層承接更多業務變化。下層離開上層應該是可以獨立存在的,例如在接口層定義的DTO不可以在下層被使用,但領域層定義的實體可以被上層使用。

2、在使用充血模型時,應該符合面向對象編程原則:不要隨意的將一些能力都充到領域實體模型中。以“菜”爲例,重量和規格是“菜”的自身的屬性,激發味蕾是“菜”的能力,“菜”可以維護自身的持久化狀態。但是、請注意、“菜”不可以“炒菜”,因爲“炒菜”的時候,“菜”還沒有出現呢,“菜”不是自己的上帝,“菜”需要被做出來,所以“菜”被做出來之前是沒有“菜”的,這是個時間上的概念,不要錯把“炒菜”的能力放在“菜”的身上。“炒菜”用到的“水+電+氣+食材+調料+廚具”不應該是“菜”的屬性範圍,這些元素都在“廚房”的範圍中,不要讓領域的模型包含不屬於自身的元素,領域的實體模型只是領域的一部分,只用於實現通用的模型能力

3、接口層和依賴層是與領域無關的:他們是與技術相關的層級,不屬於任何領域,這兩層不能包含業務邏輯。有時候我們可以把接口層拆爲兩層(接口層、應用層),但是我不建議這樣做,我們沒有必要把很輕的一層再次拆分。我們也可以把依賴層拆分爲兩個(領域模型依賴、其他依賴),我非常建議這樣做,因爲領域模型依賴的資源不會被其他領域使用,拆開之後可以有效限制領域模型的依賴,

4、領域層是與環境無關的:無論某個領域是應用還是模塊,都應該是完整的。應該具備獨立的用例層和獨立的模型層,即使多個領域在同一個應用當中,也要按照他們是分別獨立去看待,無論某個領域是應用還是模塊,領域對外部的交互,不可以繞過依賴層和接口層。

5、領域應該自治性的:把一個領域拆分爲子域、子子域..... 無限拆分,子域就不完整了。或者沒有按照功能相關性拆分,也可能破壞領域的完整性,不完整的領域不符合自治性原則。所以、不完整的領域不會單獨存在,所以、當一個領域的內部子域不具備獨立性時,子域之間不必嚴格解耦,不需要通過依賴層訪問本領域的其他子域,他們之間可以直接調用

6、領域用例層和領域模型層是兩個層級:領域用例層指的就是領域服務層,不建議將領域服務與領域模型放在同一層,這可能會導致邏輯的分散(一部分在領域服務層、一部分在領域模型層)。如果將業務邏輯寫在領域模型中,會導致業務邏輯進一步下沉,業務邏輯的不確定性太大,是不適合下沉的,是違反分層架構原則的。領域模型對應的是實體、領域服務對應的是用例,分開就是更有效的限制措施。

7、領域用例層只能承接符合自身領域的用例:我們劃分出領域的目的,就是爲了區分每個領域的職責所在,因此他們必須嚴格按照職責辦事,我們在之前已明確了用例和領域之間的關係,需要嚴格遵守。 如果出現跨領域的編排,請在接口層串聯。如果依賴其他領域的功能,請把被依賴的功能邏輯放在其他領域中。

8、領域模型層遵循最小依賴原則:只可以依賴必要的資源,必要資源指的是領域模型實現自身能力需要的資源,不包括實現業務邏輯依賴的資源。例如領域模型需要依賴DB完成持久化,可以依賴數據訪問資源,但不應該依賴其他領域資源、不可以依賴RPC資源等。 最好的做法就是將領域模型依賴的資源單獨拿出來,並且與領域模型放在一起。保持領域模型層的獨立性,在多個領域應用共享領域模型時,方便使用共享內核的設計模式。



2.4 微服務劃分

服務劃分以領域劃分爲參考,主要看我們要拆分到什麼粒度,不建議將幾個領域放在同一個服務中,不建議把一個完整的領域拆分爲幾個不完整的微服務

產出物:微服務



例如餐廳:是有必要拆分的,餐廳的“菜品域”,“訂單域”,“廚房域”有獨立的問題空間,是具備自治性的。

例如廚房:是沒有必要拆分的,廚師與刀工的耦合非常高,他們都在做飯,分開之後是不完整的,分開就是沒有必要的。



所以餐廳被拆分爲:廚房、菜品、訂單,三個微服務。基於此、我們單獨拿出餐廳門面服務作爲接口層應用,再單獨拿出餐廳基礎服務作爲水電煤氣的應用。

一般情況下,依賴層不會作爲單獨的服務提供,會被以組件的形式嵌入到其他服務中。





 



3、功能設計(用例實現)

如果說領域設計是餐廳的設計師、架構設計是餐廳的建築師、那麼功能設計就是餐廳的廚師。

任何設計都要落地到功能設計,如果廚師不守規則,偏偏要去洗手間洗菜,最後的結果依然是一團亂,最終會導致前面的所有設計泡湯。

功能設計是實現 “面向擴展開放、面向修改關閉” 的途徑,

功能設計是爲研發提供的落地支撐。



3.1 功能的概念

功能迭代時,功能會發生一些變化,所以他的含義是可能變化的,所以我們需要再次審視功能的概念,及時加以調整。

例如、我們實現了一個“做蛋炒飯”的功能,後來又實現了一個“做辣椒炒蛋”的功能,那麼我們應該將功能升級爲“炒菜”,甚至是“製作菜品”等。

結合相關功能,系統性思考和抽象,明確功能的概念,是功能設計的前提。

產出物:更新語言庫,更新腦圖



3.2 用例的位置

我們在1.3用例分析章節,明確了用例與角色的關係,在1.4領域劃分章節,明確了用例與領域的關係。

然而一個新功能的加入,我們仍然要再次評估,以確保他處於正確的位置。按照之前的做法,根據功能相關性確認用例的領域,根據角色相關性確認用例的領域服務

產出物:更新用例圖



3.3 事件風暴

事件風暴常用於梳理業務流程,適用於解構跨領域的複雜業務,感興趣的朋友可以去自行學習。

但是、對於領域內的單功能,稍有複雜的時候,我們可以採取簡化版事件風暴的方法,從而獲得如下信息:

將功能拆分爲多個子功能(步驟)。(在後續使用)

步驟對應的角色+角色身份。(在後續的3.6章節落地)

步驟的串聯流程+領域事件。(在後續的3.6章節落地)

步驟依賴的實體。(在後續的3.7章節落地)

產出物:事件風暴模型



3.4 用例分析

我們暫且收回思路,首先要關注共性和差異問題,以實現功能複用或擴展。

- 確認用例的泛化+差異點,實現功能的擴展。

- 尋找共同包含的步驟,實現邏輯的複用。

產出物:用例分析圖



例:製作菜品(做大拌菜、做鐵鍋燉、做炒雞蛋、做蒸米飯、做炒米飯)





 



3.5 用例實現類(領域服務類)結構圖

首要關注點是領域服務類的結構問題,結構決定了擴展,我們需要先達到“面相修改關閉,面相擴展開放”的目的。

領域服務的類結構圖是用例圖的映射,服務類結構圖反向映射了角色的身份,進一步反向印證了上文的觀點。

出物:用例層的類結構圖





 



3.6 用例流程圖

我們接回思路,更進一步,將事件風暴模型落實到代碼層面。

我們將步驟分配到實現類中、步驟就是該類的一個方法,進一步明確由哪個類和方法來實現該步驟,從而就規定了步驟所在的領域服務。再將步驟和領域事件串聯起來,規定了業務實現流程

在確認步驟所在位置的時候,根據角色身份相關性定位步驟的具體實現類。

推薦使用泳道圖表達上述內容,泳道的縱向組件是領域服務類,領域服務承接了所有子功能,流程圖也需要體現所有的步驟,這是用例層的橫向交互。

程序流程就是業務流程的映射,步驟分佈體現了角色身份的差異

產出物:用例流程圖



以炒雞蛋爲例,其用例流程圖如下





 



3.7 活動圖(時序圖)

進一步將事件風暴模型落實到代碼層面,我們使用時序圖,體現依賴和調用關係,規定了步驟與領域實體模型的關係,說明該步驟影響了誰。

時序圖體現了領域服務內部的縱向交互,爲了簡便、我們可以收起領域服務類(用例層)的泳道。

產出物:時序圖、活動圖





 




在本篇文章中,通過三大步驟闡述了映射辦法,讓軟件系統成爲真實業務的說明書,軟件系統似乎在對我們說“誰?在哪?做了什麼事?影響了誰?是怎麼做的?有什麼差異?”。例如我們畫的圈成爲了應用名或包名,圈中的領域模型圖成爲了實體類+數據模型,圈中的用例圖成爲了領域服務和方法,功能流程成爲了程序調用鏈,功能步驟成爲了方法,領域服務類結構反向體現了角色身份,也體現了不同身份的差異...... 系統就是業務、業務就是系統、兩者可以相互映射。

DDD的概念有很多,到底什麼是DDD?是思想嗎?是方法論嗎?每個人都有自己的理解。在我看來、DDD是一套系統化的辦法,無法用幾句話說清楚,故而以此分享DDD的落地模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章