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 用例的位置
我們在領域分析章節,已明確了用例與角色的關系,用例與領域的關系。
然而一個新功能的加入,我們仍然要再次評估,以確保他處于正確的位置。
產出物:更新用例圖
3.3 事件風暴
我們需要深入功能的細節,首推的方法是事件風暴,適用于解構復雜功能。
事件風暴的作用并不限于功能分析,只是我覺得很適用于功能分析,事件風暴的一張圖包含很多內容,正好是功能設計所需要的。
將功能拆分為多個子功能(步驟)。(在后續使用)
確認參與該步驟的角色和領域。(在后續的3.6章節落地)
確認步驟的串聯流程和領域事件。(在后續的3.6章節落地)
確認參與該步驟的領域實體。(在后續的3.7章節落地)
產出物:事件風暴模型
3.4 用例分析
我們暫且收回思路,首先要關注共性和差異問題,以實現功能復用或擴展。
- 確認用例的泛化+差異點,實現功能的擴展。
- 尋找共同包含的步驟,實現邏輯的復用。
產出物:用例分析圖
例:制作菜品(做大拌菜、做鐵鍋燉、做炒雞蛋、做蒸米飯、做炒米飯)

3.5 用例實現類(領域服務類)結構圖
首要關注點是領域服務類的結構問題,結構決定了擴展,我們需要先達到“面相修改關閉,面相擴展開放”的目的。
類結構圖是用例分析圖的一種映射,類結構圖反向映射了角色的身份,進一步說明了領域服務應該按照角色劃分。
產出物:用例層的類結構圖

3.6 用例流程圖
我們接回思路,更進一步,將事件風暴模型落實到代碼層面。
我們將步驟分配到實現類中、步驟就是該類的一個方法,進一步明確由哪個類和方法來實現該步驟,從而就規定了步驟所在的領域服務。再將步驟和領域事件串聯起來,規定了業務實現流程。
步驟就是子功能,在領域分析中已經體現了步驟所在的位置,使用功能相關性定位領域,使用角色相關性定位子域和服務,使用身份相關性定位服務實現類。
推薦使用泳道圖表達上述內容。泳道的縱向組件是領域服務類,領域服務承接了所有子功能,流程圖也需要體現所有的步驟,是用例層的橫向交互。
程序流程就是業務流程的映射,步驟的分布就是角色身份差異的映射。
產出物:用例流程圖
以炒雞蛋為例,其用例流程圖如下

3.7 活動圖(時序圖)
進一步將事件風暴模型落實到代碼層面,我們使用時序圖,體現依賴和調用關系,規定了步驟與領域實體模型的關系,說明該步驟影響了誰。
時序圖體現了領域服務內部的縱向交互,為了簡便、我們可以收起領域服務類(用例層)的泳道。
產出物:時序圖、活動圖

在本篇文章中,通過三大步驟闡述了映射辦法,讓軟件系統成為真實業務的說明書,軟件系統似乎在對我們說“誰做了什么事、影響了誰、是怎么做的、不同角色身份有什么差異......等等”。例如我們畫的圈成為了應用名或包名,圈中的領域模型圖成為了實體類+數據模型,圈中的用例圖成為了領域服務和方法,功能流程成為了程序調用鏈,每個步驟都成為了方法,領域服務類結構反向體現了角色身份,也體現了不同身份的差異...... 系統就是業務、業務就是系統、兩者可以自動化映射,這是完美的餐廳嗎?
DDD的概念有很多,然而、概念如何應用呢?到底什么是DDD?是思想嗎?核心思想是什么?是方法論嗎?具體的方法是什么? 一些人認為DDD沒有用、一些人認為DDD難以落地,每個人都有自己的理解。在我看來、DDD是一套系統化的辦法,無法用幾句話說清楚,所以我需要借助這篇案例來表達,以此分享DDD的落地模式。
好的、歡迎多多點贊并轉發給好友哦。
作者:京東科技 董健
來源:京東云開發者社區 轉載請注明來源
浙公網安備 33010602011771號