天天看點

領域驅動設計(DDD)在百度愛番番的實踐

領域驅動設計(DDD)在百度愛番番的實踐

導讀:領域驅動設計(Domain Driven Design - DDD)起源于2004年Eric Evans出版《領域驅動設計》,相比于在國外IT圈享有盛譽且行之有效不同,國内IT圈了解DDD的人很少,落地實踐的少之又少。最近幾年随着微服務架構的普及和中台的興起,DDD也成了各大技術論壇和微信公衆号文章裡經常談起的話題。

DDD的熱度是起來了,但業界介紹DDD的資料大多偏理論,缺乏生産項目可借鑒的實踐經驗。是以大多人讀了很多DDD材料後還是一臉懵,怎麼衡量DDD帶來的價值?老闆能同意搞DDD嗎?什麼樣的業務和團隊适合DDD?DDD跟網際網路強調的小步快跑快速疊代能搭嗎?如果要實踐DDD産研團隊都要做些啥?研發寫代碼跟平時有什麼不一樣?本文結合百度愛番番産研團隊在過去一年多經曆的從探索、推廣到全面落地DDD的過程,嘗試回答上述問題,力求給大家帶來一些借鑒意義。

全文約9500字,預計閱讀時間25分鐘。

1  初心:以客戶為中心,産研團隊如何高效傳遞需求

百度愛番番圍繞營銷拓客和銷售提效幫助企業收集、擴充、清洗、培養、跟進和轉化線索。一方面愛番番的業務特點是典型的企業級(ToB)業務,具有一定的複雜度。業務對象多,單個業務對象提供的功能多,單個功能面向的場景多,業務對象之間組合出來的業務流程多。并且會随着傳遞的功能越多而變的越複雜。另一方面産品處于爬坡階段,功能需要快速疊代傳遞到客戶,進而快速獲得客戶的回報。産研團隊在資源一定的情況下如何高效傳遞更複雜的需求成為了主要沖突。

分析目前階段需求疊代過程中的問題,可以總結為以下幾類問題:

業務邏輯不能從産品團隊精準傳遞到研發團隊,有時研發進行了一段時間開發才發現需求了解有偏差,導緻需要重新跟産品經理讨論需求。

産品團隊和研發團隊對于業務複雜度沒有的認識不統一,産品經理認為一個需求的開發不難,理應在較少時間内開發完畢。

研發團隊面對需求增長和變化時,缺乏對業務邏輯的抽象,往往開發一個需要點需要改動多處,容易出錯且開發效率低,代碼維護性差。

需求文檔和代碼邏輯不比對,線上功能的業務邏輯為什麼實作成那樣沒有依據可查,領域知識得不到沉澱,團隊得不到可持續成長。

2  探索:找到适合的開發模式

上述問題集中展現在兩個方面,一是産品屬于企業應用類,功能本身複雜,如何讓産研團隊快速了解業務、快速傳遞。二是如何讓領域知識能夠比較準确的得到開發實作,讓代碼有比較好的可維護性。借鑒業内處理複雜企業級軟體的開發經驗,加上部分團隊成員曾經有過DDD使用的經驗,團隊決定嘗試運用DDD設計思想來指導産研團隊的日常需求疊代。

DDD是一種圍繞領域模組化來解決複雜業務傳遞的設計思想。讀者不妨自問幾個問題,什麼是複雜?什麼是領域模組化?

複雜可能是現狀業務就複雜,也可能是業務日漸演變成複雜。複雜來自規模在變,比如幾個業務對象的邏輯不複雜,幾十上百個業務對象就會變得錯綜複雜。複雜來自結構化不足,比如下圖所示,結構化的中國結比非結構化的意大利面更有序、易于大腦了解。此外,一旦協同方多了,如何協同不同團隊完成軟體傳遞也是一種複雜。

領域驅動設計(DDD)在百度愛番番的實踐

領域模型跟技術毫無關系,而是為了更有結構化的拆解和表達業務邏輯。業務邏輯來自現實世界裡的具體場景,涉及可視畫面、操作動作和流程。要準确表達業務邏輯需要先講清楚每個概念是什麼,再建立概念之間的聯系,基于這些關系再組合出更多的流程。概念、聯系、流程就是領域模型。圍繞領域模型去表達業務時也自然而然地把技術實作細節分離出去了。後續代碼實作就是将業務架構映射到系統架構的過程,以後業務架構調整了能快速的調整技術架構。

DDD中表示業務邏輯的領域概念是:實體、值對象、領域服務、領域事件。這意味着所有領域邏輯都應該在這四種對象裡,統一稱為領域模型對象,這将極大減少業務邏輯的蔓延。

引入聚合進一步封裝實體和值對象,讓領域邏輯更内聚,起到邊界保護的作用。聚合的引入使得業務對象間的關聯變少。如何設計聚合見下面實踐部分。

圍繞聚合的操作引入工廠和資源庫。工廠負責複雜聚合的建立,資源庫負責聚合的加載、添加、修改、删除。聚合内的實體狀态變更通過領域事件來推動。

引入應用服務,對領域邏輯編排、封裝。供上層接口層調用。一個應用服務就是一次編排,一次編排就是一個使用者用例。

領域驅動設計(DDD)在百度愛番番的實踐

DDD包含戰略設計、戰術設計、技術實作三個部分。戰略設計側重于高層次、宏觀上去劃分限界上下文,而戰術設計則關注使用模組化工具來細化上下文,通過領域模型來表達業務。技術實作主要通過分層架構來隔離領域模型代表的業務邏輯和技術細節。一個整體過程大緻包括:宏觀劃分各領域 → 領域内劃分限界上下文,定義上下文之間的關系 → 上下文内分析業務,識别領域概念,定義合适的領域概念 → 通過分層架構實作編碼,并驗證領域模型的合理性,必要時重新回到前面步驟重構領域模型。

戰略設計是團隊上司層或業務負責人關心的,該步驟需要針對産品願景、業務要解決的問題域,規劃核心域、通用域、支撐域,做合适的資源投入。

領域代表現實世界的特定問題和解決方案的集合,比如銷售領域、營銷領域。DDD裡的限界上下文(Bouded Context)是對領域的軟體實作,比如線索系統、商機系統就是銷售領域内的限界上下文。限界上下文定義了解決方案的明顯邊界,邊界裡的每一個領域概念,包括領域概念内的屬性和行為都有特殊含義。出了限界上下文這個邊界這層含義就不複存在。

1:根據相關性做歸類。一般是優先考慮功能相關性而不是語義相關性,比如建立訂單和支付訂單都是訂單語義,但功能相差比較大,應該劃分為兩個限界上下文。

2:根據團隊粒度做裁剪、根據技術特點做裁剪。一些通用的技術功能應該盡可能歸攏到一個限界上下文,比如每個業務限界上下文都有監控,但監控能力應該歸攏到監控限界上下文。

微服務是包含高度相關功能的一個開發部署單元,有自己的技術自治性包括技術選型、彈性擴縮容、釋出上線頻率等,有自己的業務演變自治性。BC是根據領域邏輯的内聚情況形成的一個整體。一個微服務可以包含一個或多個BC,到底包含幾個?需要根據團隊大小、BC複雜度和技術特性來定。

DDD設計思想裡領域模組化是最核心的一步,該階段主要目标是提煉和定義出領域模型和之間的關系。

模組化就是設計的過程,模組化的過程就是梳理、走查業務邏輯,拆解為要解決的問題和涉及的業務場景、業務流程、業務概念,在這個過程中形成對應的領域概念。

如果團隊對于業務比較陌生适合采用事件風暴方法進行梳理;如果團隊對業務比較熟悉,如果業務流程相對簡單,則可以采用四色模組化法進行業務梳理。采用這些分析業務的方法可以保證産研團隊對業務邏輯的了解在一個水準上。

在完成了實體和值對象的設計後,有的時候會發現有些概念其實在領域上是存在的,但設計和代碼裡沒有Class來展現,可能僅僅是一個基本類型參數加上散落的對該參數的判斷檢驗邏輯,這個時候還需要思考應該把這個概念顯性化,定義專門的Class并包含相應邏輯,入出參以相應Class為類型。但凡業務代碼邏輯包含了一堆if-else,這時候需要考慮盡可能給這段邏輯模組化成一個領域概念。

比如CRM系統裡判斷一條線索是否為推廣線索需要看線索的管道屬性是否來自推廣平台,那麼比較好的方式是這段邏輯用"推廣線索"這個概念來顯性表達,而不是淹沒在代碼裡不容易了解和維護。

為了解決業務邏輯銜接的問題引入了統一語言。每個業務名詞的含義具有明确的定義,産品和研發都統一認識。沒有統一語言的溝通嚴重缺乏效率。比如CRM線索的概念,沒有統一語言的時候每個人的了解不一樣,有的人了解為有過咨詢記錄的訪客是線索,有的人了解為留下過聯系方式的訪客是線索,有的人了解為有購買意願的訪客是線索等等。

有了統一語言描述,每個概念就有了明确定義,可以節省非常大的溝通交流成本。并且這個概念也同樣應用在相關的需求文檔、設計文檔、代碼編寫中。每個概念從引入到日常交流,從需求文檔到代碼實作都有了一緻的表達,代碼實作和需求描述的真實度高,可了解性和可維護性就變好了。

為了讓代碼實作圍繞領域模型開展,盡量降低業務代碼和純技術選型代碼的耦合,DDD引入了分層架構。確定了最核心的領域層不依賴其他層,反過來讓領域之外的代碼依賴領域代碼,降低了技術更新帶來的影響。

架構内定義不同領域概念需要實作的接口,比如實作了聚合根接口的實體類就成為了聚合的根實體。定義了異常管理規範,不同的分層應該抛出什麼類型的異常。定義了資料通路的資源庫接口等等。

領域事件是對領域内發生的活動進行的模組化,即聚合内的實體狀态變化的一個載體。DDD提倡限界上下文間盡量解耦,盡可能使用釋出訂閱領域事件的協作模式進行上下遊解耦。

傳統的業務開發模式裡,研發受到關系型資料庫設計範式、ER圖等影響深遠,在做軟體詳細設計過程中往往先想到如何設計對應的表結構,由此倒推出業務邏輯代碼該如何組織。這就是典型的資料模型驅動設計,或者叫面向資料表設計程式設計。資料模型設計關注的是資料存儲,資料盡量不要備援,控制表數量不膨脹,更多考慮資料的擴充性,比如新加一個字段盡量不要在幾張表都加,能用一個字段表達就不用兩個字段。

這樣的思維跟DDD是相反的,DDD優先考慮領域概念的業務語義表達,具有獨立業務概念的東西會盡量抽象成一個内聚的領域對象。領域對象不僅僅有屬性,還有該有的行為。

是以,基于資料模型驅動的設計結果往往是:

1. 業務邏輯代碼非常過程式,領域實體隻包含一堆屬性,隻是資料表的映射,沒有業務行為。也就是常說的隻有getter和setter方法的貧血對象。非常缺乏領域概念的表達,業務邏輯散亂。比如值對象的設計在DDD裡是一個類,在資料模型設計裡往往是其他類的幾個屬性。

2. 聚合是DDD最小的複用單元,粒度更粗。資料模型設計裡領域實體的數量跟表數量一一對應,資料表是最小的複用單元,粒度太細。導緻業務邏輯對應的實作類需要通路很多的領域實體,實作類之間的調用關系發散而錯綜複雜。下圖是貧血模型和DDD富血模型的差別。

領域驅動設計(DDD)在百度愛番番的實踐

3. 資料表的關系表達很受限,具有主從關系的表之間很難看出主從。在DDD裡聚合和聚合内的實體、值對象之間的關系在代碼層面有顯示的表達。

當然,DDD思想裡不是說不用考慮資料表設計,而是要優先考慮領域概念的識别和模組化。表設計需要服務于領域模型的設計,是技術實作的細節。是以明白DDD和資料模型驅動設計的差別反過來能更好地了解DDD。

3  實踐:案列分析

以愛番番業務中"線索"功能舉例,線索管理功能特别多,有建立、清洗、配置設定、打标簽、跟進、回收、退回和轉化等十幾個管理動作。僅線索建立就分為手工錄入建立、檔案導入建立、營銷系統的背景自動建立、開放平台建立,建立還分為單個建立和批量建立等等。線索這個對象跟其他對象比如客戶、商機等關聯組合出來很多場景和流程。

規劃階段需要考慮産品願景和服務藍圖,需要劃分出産品的核心領域,支撐領域,通用領域。如果從0到1開發産品的話規劃階段需要做很多的工作,比如開發一個CRM産品需要考慮産品願景和服務藍圖,需要聚焦到哪些業務領域,是售前、售中還是售後?售前還可以細分為營銷領域還是銷售領域等等。百度愛番番緻力打造易用的、靈活可配的線索管家功能。是以銷售領域的線索功能自然是核心子產品。需要提供什麼線索功能?需要通過分析階段來拆解。

分析階段是基于業務流程和功能分析出具體的業務對象,不同的業務對象歸屬劃分到限界上下文。因為線索功能複雜,團隊對于線索功能認知不一,有必要讓相關人員一起采用事件風暴方法來分析和梳理業務。事件風暴認為事件流很⼤程度上反映了現實業務邏輯,參與人員基于領域事件發生的時間線,把事件的前因後果逐漸挖掘出來。整個過程包含識别領域事件、決策指令、領域名詞三個步驟。通過嘗試回答這幾個問題:這個業務涉及的系統産生了什麼變化?變化由哪個角色通過什麼方式觸發的?系統變化産生了哪些結果?

基于上述步驟,領域專家和相關人員針對線索業務進行事件風暴的結果為:

領域驅動設計(DDD)在百度愛番番的實踐

事件風暴關鍵圖例:

領域驅動設計(DDD)在百度愛番番的實踐

事件風暴實踐過程的幾點tips:

事件流幾乎等同業務邏輯,以此來推敲業務邏輯的嚴密性,有果必有因。

緊扣事件要素:事件、規則、名詞、指令、角色。

命名:緊扣業務,不參雜技術元素,警惕使用泛泛的詞彙,盡可能地消除命名的⼆義性。

優先關注happy-path即正常路徑,聚焦核心領域裡的路徑。

事件風暴不是一蹴而就,保持疊代更新。

基于事件風暴的結果,需要把領域名詞和規則等劃分到合适的限界上下文。根據前面介紹的如何劃分限界上下文的方法,線索相關功能劃分為幾個限界上下文合适呢?這個時候需要看業務邏輯的複雜程度,還要結合團隊規模大小。由于線索功能包含很多業務邏輯,線索歸集和建立、線索的配置設定、線索的跟進等都可以成為一個獨立的限界上下文。定義好限界上下文後還需要定義不同限界上下文的協作關系。一般情況下如果業務允許的情況盡量選擇通過領域事件來協作。根據《領域驅動設計》所述常見的協作關系還包括開放主機服務(即通過暴露接口)、共享核心、防腐層等9種。微服務架構下的限界上下文之間的關系比較常見的有領域事件、開放主機服務、防腐層等。

設計階段就是把分析階段産出的領域名詞,領域事件,決策指令用DDD領域概念來承接,并細化每個領域概念的資料和行為。這也是一種領域模組化的過程。

建議的模組化過程是:

業務需求的分析過程自上而下,由業務流程,到使用者用例,到領域模型。而設計過程是自下而上的。從領域元素設計開始,最後才是應用服務的編排。

建議設計優先級是先值對象 → 再實體 → 再聚合 → 再領域服務→ 最後是應用服務,優先考慮領域是否應該為值對象,其次是否為實體,劃分出聚合。不屬于實體或值對象中的領域行為放到領域服務,需要協調聚合的領域行為設計為領域服務或者應用服務。

任何業務代碼邏輯優先映射到原子性的領域模型,比如值對象、實體、領域事件、資源庫接口、外部适配接口,其次再映射到組合性領域模型,比如領域服務、應用服務。

模組化過程中經常會被問到的問題有:

1 值對象可以定義自己的行為嗎?

可以,盡可能把屬于值對象自己的行為放到值對象裡。比如聯系方式定義成一個值對象,如果它的校驗隻依賴自身資料,那校驗行為應該屬于在聯系方式這個值對象。

2 聚合該設計為多大粒度?

聚合設計要盡量小,如果一個實體不是根實體,但同時需要被外界直接通路到,那麼這個實體不應該在這個聚合中,應該獨立成新的聚合。

3 一個聚合如何通路另外一個聚合?

隻有聚合根才是通路聚合邊界的唯一入口,是以一個聚合需要通過另一個的聚合的聚合根來通路它,聚合根可以了解為聚合的根實體的Id。

4 應用服務與領域服務的差別?

領域服務處在分層架構的領域層,是領域邏輯的一部分。應用服務處在應用層,負責領域模型的編排。當業務邏輯不屬于任何聚合時,應該考慮用領域服務來封裝這些邏輯。比如判定訂單是否重複,應該屬于訂單限界上下文的一種業務邏輯,訂單聚合本身不能判斷是否重複,是以訂單判重應該定義為領域服務。

5 應用服務可以直接調用聚合和資源庫嗎?

可以,可被應用服務編排的對象包括聚合、資源庫、領域服務和适配接口。

6 領域事件内容是包含整個聚合裡的資訊,還是身份辨別資訊(訂閱方再通過單獨接口根據辨別進行查詢),還是隻包含聚合中一些特定的資訊?

領域事件是用于跟其他聚合協作,事件内容不應是整個聚合,而是經過裁剪的特定資訊。

根據分析階段的産出結果,需要把領域名詞、規則映射到領域模型。主要幾個線索相關領域對象如下圖示:

領域驅動設計(DDD)在百度愛番番的實踐

傳統的接口-邏輯-資料通路三層架構裡,業務邏輯層的XxxServiceImpl類是個上帝類,往往通過過程式業務邏輯實作。前幾行代碼做校驗,接下來做資料類型轉換,然後是業務處理邏輯的代碼,中間穿插着通過接口或者dao擷取更多的資料;拿到資料後,又是類型轉換代碼,然後接着一段業務邏輯代碼,最後可能還要落庫、釋出消息等等。這樣的代碼參雜了太多不同的代碼,非常難以維護。

業界自從DDD的分層架構提出後陸續出現過洋蔥架構、六邊形架構、整潔架構等,其目标都是為了分離業務和技術,保證領域模型的純粹性。下圖是結合業界架構實踐後定制的分層架構,具有以下幾個特點:

接口層負責對外暴露各種協定的接口比如http、tcp,轉換成應用服務能認識的協定。

核心的領域層不依賴其他層,通過資源庫包下的接口定義做到依賴倒置,接口參數不能展現具體技術實作細節,領域模型裡的實作邏輯隻依賴接口。這樣做到對領域邏輯的一層防腐。本層裡以聚合為機關放置代碼,便于以後系統拆分,以聚合為機關。

應用層定義應用服務,一個接口對應業務場景的一個用例。此外應用層還可以處理橫切面事務比如啟動資料庫事務。

基礎設施層完成資源庫的實際實作,以及領域層定義的其他接口的實作如對外部服務的通路,領域事件釋出到消息隊列中間件等。

分層架構還定義了每層的項目包結構,不同的領域概念和資料對象相應的命名規範。

領域驅動設計(DDD)在百度愛番番的實踐

實作階段經常會被問到的問題有:

1

每層應該用什麼類型資料對象承載和傳遞資料?

如上面分層架構圖所示,接口層和應用服務層用DTO對象傳遞資料,領域層隻能見到領域對象即聚合、實體Entity和值對象VO。應用服務層負責把DTO對象轉換成領域對象傳輸到領域層。基礎設施層用PO表示資料表,跟領域層調用時需要把PO和領域對象互相做轉換。

2

repository和dao的差別?

3

領域事件的釋出應該在領域層還是應用層?

隻要不會破壞各層的依賴順序,在哪釋出都行。取決于領域事件定義在哪層?一般推薦定義在領域層的聚合内。當然即便在應用層釋出事件也不會破壞依賴方向。是以聚合、領域服務、應用服務都可以釋出事件。

以java代碼為例,DDD骨架代碼包含了分層架構,每層就是一個maven pom項目,根據用途定義好了多層包結構,每個領域對象和資料傳輸對象都有具體的命名方式。基于自研的ddd-framework規範了不同領域對象需要實作的接口或繼承于特定的基類。

總之,盡可能做到了能根據需求文檔裡的業務邏輯很快找到代碼所在之處,讓不同的代碼待在應該待的分層和包下面。團隊成員開玩笑說,現在開發業務代碼就像在做填空題,簡單直白。

領域驅動設計(DDD)在百度愛番番的實踐

目前百度愛番番的新服務預設都會在符合DDD架構的骨架代碼基礎上開發,存量的核心子產品也進行過DDD改造。全面實施DDD後産研團隊目标更對齊,協作效率更高,收獲了很多收益,包括但不限于以下幾點:

産研團隊協同成本降低,領域知識得到積累和沉澱。統一語言的使用和維護極大提高了大家對齊的成本。

業務語義得到顯性表達,業務邏輯内聚可複用程度提高,避免了很多散彈式修改和發散式修改。一個需求不用改多個地方,多個需求也不用幾個研發集中改同一個地方。

限界上下文的劃分從業務合理性出發,進而微服務的劃分會更合理,減少了團隊間的耦合和不必要的協同代價。

接口數量精簡、可控。由于業務代碼聚焦領域模型,邏輯内聚,複用性高,急劇減少了接口數量,降低接口維護成本。

通過預定義好的腳手架建立符合DDD規範的代碼骨架,提高了新服務開發的效率。

代碼可讀性高,不是代碼作者也能快速定位到代碼位置,代碼設計能夠得到傳承,可維護性也提高了。

新人熟悉新業務和新代碼的速度極大提高,業務和技術知識的轉移代價減低。

從需求到傳遞的一次典型軟體開發流程包括收集提煉需求、需求分析、業務&技術設計、代碼實作、測試上線等環節。如何結合軟體開發流程,每個流程階段具體要做什麼、怎麼做,特别在編碼落地階段該有什麼保障措施?愛番番産研團隊在落地過程中逐漸總結出了一套行之有效的DDD實施指南。包括規劃、分析、設計到實作四個階段對應的方法和産出等實施要點。

領域驅動設計(DDD)在百度愛番番的實踐

4  結語:殊途同歸、沒有銀彈

DDD一方面使用分而治之的思想,引入劃分領域、限界上下文、子產品分層、劃分聚合在不同層次、不同粒度來降低問題的複雜度。另一方主張聚焦領域邏輯,通過不同手段來減少業務和技術的耦合。是以DDD隻是大部分軟體設計思想一種,軟體設計的本質都是為了高内聚低耦合。但是DDD并不是萬能的,不是所有業務開發場景都适合用DDD。有些簡單業務場景不使用DDD反而更恰當。因為DDD有較高的學習門檻,需要整個團隊形成統一認識和協同,需要相應的編碼規範和架構落地。是以學習和落地DDD時要時刻記住自己的出發點是為了應對現在或者将來的複雜業務領域而來。不必太拘泥于某些點是否遵守了DDD原則,如果覺得用了DDD會比沒有用好一點點,也值得邁出這一步。

愛番番産研團隊始終秉持“以客戶為中心”的理念,運用DDD設計思想建構統一的業務模型,實作業務功能的複用和融合。随着愛番番業務的發展,我們相信DDD帶來的收益會更大。今後我們會從産品、技術、流程群組織方面持續關注能有效解決軟體工程複雜性問題的方法。

繼續閱讀