DDD 落地實踐 - 架構師眼中的餐廳

本文以餐廳場景爲敘事主線,以領域驅動爲核心思想,結合架構設計與功能設計方法論。是從領域分析到落地的全過程案例,內容偏重於落地,因此不乏一些探討,歡迎指正。

文章較長、全程乾貨、耐心讀完、必有收穫。

本文不針對餐廳的實現細節,重在探討設計思想和方法。

1、領域設計

讓我們拋開技術人員的本能技術視角、站在純業務視角來分析領域問題。

領域設計的核心是分而治之,目的是實現業務領域的自治性。

就像你平時不會將枕頭和被子放在廚房或衛生間一樣,你的牀上不會放着大米白麪,否則你想睡覺是一件很複雜的事情,軟件系統也是如此,這就是我們要解決的問題。

1.1 宏觀流程

假如我要設計一個餐廳,由於分而治之的需要,我會首先從宏觀流程去分析,可以幫我們迅速找到重要的區域。



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

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

1.2 統一語言

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

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

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

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

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

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

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



1.3 用例分析

在這一步、我們使用相對宏觀的分析,不需要進入用例的細節分析,掌握角色與行爲之間的關係,理清誰在做什麼,角色的職責差異是什麼。

產出物:用例圖

以做菜爲例,如圖

1.4 領域劃分

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

功能相關性:是用例與領域之間的關係,任何業務的領域都是由一套用例組成的,所以領域劃分以功能相關性爲主,例如與做菜相關的用例都應該歸屬於廚房,所以我們確認了廚房域,確認了廚房域包含的用例,這是很自然的事。

角色相關性:其次是角色,常用於劃分子域,某個區域涉及多個角色參與,可以按照角色的分工,拆分爲多個子域,從而滿足不同角色的個性化需要。例如廚房的採購人員負責買菜、刀工負責切菜、大廚負責烹飪。我們就會考慮將廚房劃分爲 “採購域”、“加工域”、“烹飪域”。

通常來說,子域不具備獨立的問題空間,不會作爲獨立的領域存在。

產出物:領域、子域

以廚房域爲例,如圖



1.5 領域建模

這是大家比較熟知的階段,重點分析實體與領域之間關係(領域聚合),實體與實體的關係(OO 聚合)。

領域模型是實現功能的基石、需要有對功能的本質理解,才能找到最核心的實體,實體之間的 OO 聚合關係決定了功能的擴展性,OO 聚合是最重要的核心點。



組合、聚合

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

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

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



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

(領域服務和事件在後續的功能設計中提供)

1.6 領域上下游

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

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

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

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

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

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

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



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

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

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

我們常常使用領域事件串聯業務流程,在使用領域事件時,不止要關注點對點的解耦,更應該使業務流程符合領域上下游限定,讓各個領域獨立運行,減少領域之間的功能依賴,降低領域之間的耦合,減少業務變化帶來的影響。

2、架構設計

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

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

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

2.1 分層架構

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

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

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

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

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

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

2.2 架構映射

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

**業務領域映射:**我們將劃分好的領域,按照對應的視角映射爲對應的元素,領域模型映射到架構模型時,應該是視角對等的,如果餐廳是系統、那麼廚房就是應用,如果餐廳是應用、那麼廚房就是模塊。也應該層級匹配的,將用例的實現映射到用例層,將領域模型的實現映射到領域模型層。

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

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

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

產出物:分層架構圖

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



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



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

2.3 必要的約束

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

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

3、接口層和依賴層是與領域無關的:他們是與技術相關的層級,不屬於任何領域,這兩層不能包含業務邏輯。有時候我們可以把接口層拆爲兩層(接口層 + 應用層),也可以把依賴層拆分爲兩個(模型依賴、服務依賴)。

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

5、領域應該是最小完備的:把一個領域拆分爲子域、子子域、子子子...... 無限拆分,拆分到一定程度之後,某個子域就不完整了,不完整的子域是不可以獨立存在的。拆分不不夠或者過度拆分,都是不符合低耦合高內聚原則的。當一個領域的內部子域不具備獨立性時,他們之間不必嚴格解耦,不需要通過依賴層訪問本領域的其他子域,他們之間可以直接調用。

6、領域服務層就是領域用例層:他們倆是同一回事兒,都是用於實現領域內的用例的。不要將領域服務與領域用例視爲兩個獨立的層,也不要將領域服務與領域模型視爲同一層,否則會導致邏輯的分散(一部分在領域服務層、一部分在領域模型層、還有一部分可能在用例層),也會導致每個層的職責不明確,容易搞亂。如果將業務邏輯寫在領域模型中,會導致業務邏輯進一步下沉,業務邏輯的不確定性太大,是不適合下沉的,是違反分層架構原則的。領域模型對應的是實體、領域服務對應的是用例。

7、領域用例層只能承接符合自身領域的用例:我們劃分出領域的目的,就是爲了區分每個領域的職責所在,因此他們必須嚴格按照職責辦事,我們在之前已明確了用例和領域之間的關係,需要嚴格遵守。

8、領域模型層遵循最小依賴原則:只可以依賴必要的資源,必要資源指的是領域模型實現自身能力需要的資源,不包括實現業務邏輯包含的資源。例如領域模型需要依賴 DB 完成持久化,可以依賴數據訪問資源,但不應該依賴其他領域資源、不可以依賴 RPC 資源等。

2.4 微服務劃分

服務劃分以領域劃分爲參考,主要看我們要拆分到什麼粒度,這 應該符合低耦合高內聚原則,不破壞領域實體的聚合關係。

產出物:微服務

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

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

所以餐廳被拆分爲:廚房(Kitchen)、菜品(Category)、訂單(Order)三個微服務。

基於此、我們單獨拿出餐廳門面服務作爲接口層應用,再單獨拿出餐廳基礎服務作爲水電煤氣的應用。

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

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

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

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

功能設計是實現 “面向擴展開放、面向修改關閉” 的途徑,是指導研發落地必備環節。

3.1 功能的概念

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

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

明確功能的概念,是功能設計的前提。

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

3.2 用例的位置

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

然而一個新功能的加入,我們仍然要再次評估,以確保他處於正確的位置。

產出物:更新用例圖

3.3 事件風暴

我們需要深入功能的細節,首推的方法是事件風暴,適用於解構複雜功能。

事件風暴的作用並不限於功能分析,只是我覺得很適用於功能分析,事件風暴的一張圖包含很多內容,正好是功能設計所需要的。

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

確認參與該步驟的角色和領域。(在後續的 3.6 章節落地)

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

確認參與該步驟的領域實體。(在後續的 3.7 章節落地)

產出物:事件風暴模型

3.4 用例分析

我們暫且收回思路,首先要關注共性和差異問題,以確保功能的擴展性。

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

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

產出物:用例分析圖

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

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

專注於用例層的類設計,實現 “面相修改關閉,面相擴展開放”。

用例的類結構圖是用例分析圖的一種映射。

出物:用例層的類結構圖



3.6 用例流程圖

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

我們將步驟分配到實現類中、步驟就是該類的一個方法,進一步明確由哪個類和方法來實現該步驟,從而就規定了步驟所在的領域。

我們將步驟和領域事件串聯起來,規定了業務實現流程。推薦使用泳道圖表達上述內容。泳道的縱向組件是用例的實現類。

這是真實業務流程的映射。

產出物:用例流程圖

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



3.7 活動圖(時序圖)

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

這時候,爲了簡便、我們可以收起領域服務類(用例層)的泳道。

產出物:時序圖、活動圖



試想一下、假如把業務邏輯放在領域模型當中(例如聚合),如何實現 “面相擴展開放、面相修改關閉” 呢?

4、編碼實現

編碼實現......  我決定還是......  偷個懶吧......  哈哈哈。

但是我們回顧一下之前的內容,是否足夠了?不同的研發人員依照設計去編碼,是否會寫出不一樣的代碼?

最後、我們的目標是 “解決軟件複雜度帶來的問題”,而實現這個目標的途徑是 “設計指導研發落地”。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/iUHeZyYofp8pveJz4eR1Dg