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

圖片

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

DDD的熱度是起來了,但業界介紹DDD的資料大多偏理論,缺少生產項目可借鑑的實踐經驗。所以大多人讀了不少DDD材料後仍是一臉懵,怎麼衡量DDD帶來的價值?老闆能贊成搞DDD嗎?什麼樣的業務和團隊適合DDD?DDD跟互聯網強調的小步快跑快速迭代能搭嗎?若是要實踐DDD產研團隊都要作些啥?研發寫代碼跟平時有什麼不同?本文結合百度愛番番產研團隊在過去一年多經歷的從探索、推廣到全面落地DDD的過程,嘗試回答上述問題,力求給你們帶來一些借鑑意義。java

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

1  初心:以客戶爲中心,產研團隊如何高效交付需求

百度愛番番圍繞營銷拓客和銷售提效幫助企業收集、擴充、清洗、培養、跟進和轉化線索。一方面愛番番的業務特色是典型的企業級(ToB)業務,具備必定的複雜度。業務對象多,單個業務對象提供的功能多,單個功能面向的場景多,業務對象之間組合出來的業務流程多。而且會隨着交付的功能越多而變的越複雜。另外一方面產品處於爬坡階段,功能須要快速迭代交付到客戶,從而快速得到客戶的反饋。產研團隊在資源必定的狀況下如何高效交付更復雜的需求成爲了主要矛盾。數據庫

分析當前階段需求迭代過程當中的問題,能夠總結爲如下幾類問題:編程

  1. 業務邏輯不能從產品團隊精準傳遞到研發團隊,有時研發進行了一段時間開發才發現需求理解有誤差,致使須要從新跟產品經理討論需求。後端

  2. 產品團隊和研發團隊對於業務複雜度沒有的認識不統一,產品經理認爲一個需求的開發不難,理應在較少時間內開發完畢。微信

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

  4. 需求文檔和代碼邏輯不匹配,線上功能的業務邏輯爲何實現成那樣沒有依據可查,領域知識得不到沉澱,團隊得不到可持續成長。app

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


上述問題集中體如今兩個方面,一是產品屬於企業應用類,功能自己複雜,如何讓產研團隊快速理解業務、快速交付。二是如何讓領域知識可以比較準確的獲得開發實現,讓代碼有比較好的可維護性。借鑑業內處理複雜企業級軟件的開發經驗,加上部分團隊成員曾經有過DDD使用的經驗,團隊決定嘗試運用DDD設計思想來指導產研團隊的平常需求迭代。框架

2.1  DDD是啥?

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

1. 什麼是複雜?如何理解複雜?

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

圖片

2. 什麼是領域建模?

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

3. DDD中的領域如何理解?

  • DDD中表示業務邏輯的領域概念是:實體、值對象、領域服務、領域事件。這意味着全部領域邏輯都應該在這四種對象裏,統一稱爲領域模型對象,這將極大減小業務邏輯的蔓延。

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

  • 圍繞聚合的操做引入工廠和資源庫。工廠負責複雜聚合的建立,資源庫負責聚合的加載、添加、修改、刪除。聚合內的實體狀態變動經過領域事件來推進。

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

4. DDD領域概念詳細解釋和舉例

image.png

2.2  DDD如何開展?

DDD包含戰略設計、戰術設計、技術實現三個部分。戰略設計側重於高層次、宏觀上去劃分限界上下文,而戰術設計則關注使用建模工具來細化上下文,經過領域模型來表達業務。技術實現主要經過分層架構來隔離領域模型表明的業務邏輯和技術細節。一個總體過程大體包括:宏觀劃分各領域 → 領域內劃分限界上下文,定義上下文之間的關係 → 上下文內分析業務,識別領域概念,定義合適的領域概念 → 經過分層架構實現編碼,並驗證領域模型的合理性,必要時從新回到前面步驟重構領域模型。

1. 戰略設計

戰略設計是團隊領導層或業務負責人關心的,該步驟須要針對產品願景、業務要解決的問題域,規劃核心域、通用域、支撐域,作合適的資源投入。

1. 什麼是領域和限界上下文?

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

2. 如何劃分限界上下文?

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

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

3. BC與微服務什麼關係?

微服務是包含高度相關功能的一個開發部署單元,有本身的技術自治性包括技術選型、彈性擴縮容、發佈上線頻率等,有本身的業務演變自治性。BC是根據領域邏輯的內聚狀況造成的一個總體。一個微服務能夠包含一個或多個BC,到底包含幾個?須要根據團隊大小、BC複雜度和技術特性來定。

2. 戰術設計

DDD設計思想裏領域建模是最核心的一步,該階段主要目標是提煉和定義出領域模型和之間的關係。

1. 領域建模

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

若是團隊對於業務比較陌生適合採用事件風暴方法進行梳理;若是團隊對業務比較熟悉,若是業務流程相對簡單,則能夠採用四色建模法進行業務梳理。採用這些分析業務的方法能夠保證產研團隊對業務邏輯的理解在一個水平上。

2. 業務邏輯的顯性表達

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

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

3. 統一語言

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

有了統一語言描述,每一個概念就有了明肯定義,能夠節省很是大的溝通交流成本。而且這個概念也一樣應用在相關的需求文檔、設計文檔、代碼編寫中。每一個概念從引入到平常交流,從需求文檔到代碼實現都有了一致的表達,代碼實現和需求描述的真實度高,可理解性和可維護性就變好了。

3. 技術實現

1. 分層架構

爲了讓代碼實現圍繞領域模型開展,儘可能下降業務代碼和純技術選型代碼的耦合,DDD引入了分層架構。確保了最核心的領域層不依賴其餘層,反過來讓領域以外的代碼依賴領域代碼,下降了技術升級帶來的影響。

2. DDD框架

框架內定義不一樣領域概念須要實現的接口,好比實現了聚合根接口的實體類就成爲了聚合的根實體。定義了異常管理規範,不一樣的分層應該拋出什麼類型的異常。定義了數據訪問的資源庫接口等等。

3. 領域事件

領域事件是對領域內發生的活動進行的建模,即聚合內的實體狀態變化的一個載體。DDD提倡限界上下文間儘可能解耦,儘量使用發佈訂閱領域事件的協做模式進行上下游解耦。

2.3  DDD vs 數據模型驅動

傳統的業務開發模式裏,研發受到關係型數據庫設計範式、ER圖等影響深遠,在作軟件詳細設計過程當中每每先想到如何設計對應的表結構,由此倒推出業務邏輯代碼該如何組織。這就是典型的數據模型驅動設計,或者叫面向數據表設計編程。數據模型設計關注的是數據存儲,數據儘可能不要冗餘,控制表數量不膨脹,更多考慮數據的擴展性,好比新加一個字段儘可能不要在幾張表都加,能用一個字段表達就不用兩個字段。

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

所以,基於數據模型驅動的設計結果每每是:

1. 業務邏輯代碼很是過程式,領域實體只包含一堆屬性,只是數據表的映射,沒有業務行爲。也就是常說的只有getter和setter方法的貧血對象。很是缺少領域概念的表達,業務邏輯散亂。好比值對象的設計在DDD裏是一個類,在數據模型設計裏每每是其餘類的幾個屬性。

2. 聚合是DDD最小的複用單元,粒度更粗。數據模型設計裏領域實體的數量跟表數量一一對應,數據表是最小的複用單元,粒度太細。致使業務邏輯對應的實現類須要訪問不少的領域實體,實現類之間的調用關係發散而錯綜複雜。下圖是貧血模型和DDD富血模型的區別。

圖片

3. 數據表的關係表達很受限,具備主從關係的表之間很難看出主從。在DDD裏聚合和聚合內的實體、值對象之間的關係在代碼層面有顯示的表達。

固然,DDD思想裏不是說不用考慮數據表設計,而是要優先考慮領域概念的識別和建模。表設計須要服務於領域模型的設計,是技術實現的細節。所以明白DDD和數據模型驅動設計的區別反過來能更好地理解DDD。

3  實踐:案列分析


3.1  業務背景

以愛番番業務中"線索"功能舉例,線索管理功能特別多,有建立、清洗、分配、打標籤、跟進、回收、退回和轉化等十幾個管理動做。僅線索建立就分爲手工錄入建立、文件導入建立、營銷系統的後臺自動建立、開放平臺建立,建立還分爲單個建立和批量建立等等。線索這個對象跟其餘對象好比客戶、商機等聯動組合出來不少場景和流程。

3.2  規劃階段

規劃階段須要考慮產品願景和服務藍圖,須要劃分出產品的核心領域,支撐領域,通用領域。若是從0到1開發產品的話規劃階段須要作不少的工做,好比開發一個CRM產品須要考慮產品願景和服務藍圖,須要聚焦到哪些業務領域,是售前、售中仍是售後?售前還能夠細分爲營銷領域仍是銷售領域等等。百度愛番番致力打造易用的、靈活可配的線索管家功能。所以銷售領域的線索功能天然是核心模塊。須要提供什麼線索功能?須要經過分析階段來拆解。

3.3  分析階段

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

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

圖片

事件風暴關鍵圖例:

圖片

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

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

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

  3. 命名:緊扣業務,不參雜技術元素,警戒使用泛泛的詞彙,儘量地消除命名的⼆義性。

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

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

基於事件風暴的結果,須要把領域名詞和規則等劃分到合適的限界上下文。根據前面介紹的如何劃分限界上下文的方法,線索相關功能劃分爲幾個限界上下文合適呢?這個時候須要看業務邏輯的複雜程度,還要結合團隊規模大小。因爲線索功能包含不少業務邏輯,線索歸集和建立、線索的分配、線索的跟進等均可以成爲一個獨立的限界上下文。定義好限界上下文後還須要定義不一樣限界上下文的協做關係。通常狀況下若是業務容許的狀況儘可能選擇經過領域事件來協做。根據《領域驅動設計》所述常見的協做關係還包括開放主機服務(即經過暴露接口)、共享內核、防腐層等9種。微服務架構下的限界上下文之間的關係比較常見的有領域事件、開放主機服務、防腐層等。

3.4  設計階段

設計階段就是把分析階段產出的領域名詞,領域事件,決策命令用DDD領域概念來承接,並細化每一個領域概念的數據和行爲。這也是一種領域建模的過程。

建議的建模過程是:

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

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

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

建模過程當中常常會被問到的問題有:

1 值對象能夠定義本身的行爲嗎?

能夠,儘量把屬於值對象本身的行爲放到值對象裏。好比聯繫方式定義成一個值對象,若是它的校驗只依賴自身數據,那校驗行爲應該屬於在聯繫方式這個值對象。

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

聚合設計要儘可能小,若是一個實體不是根實體,但同時須要被外界直接訪問到,那麼這個實體不該該在這個聚合中,應該獨立成新的聚合。

3 一個聚合如何訪問另一個聚合?

只有聚合根纔是訪問聚合邊界的惟一入口,所以一個聚合須要經過另外一個的聚合的聚合根來訪問它,聚合根能夠理解爲聚合的根實體的Id。

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

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

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

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

6 領域事件內容是包含整個聚合裏的信息,仍是身份標識信息(訂閱方再經過單獨接口根據標識進行查詢),仍是隻包含聚合中一些特定的信息?

領域事件是用於跟其餘聚合協做,事件內容不該是整個聚合,而是通過裁剪的特定信息。

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

圖片

3.5  實現階段

傳統的接口-邏輯-數據訪問三層架構裏,業務邏輯層的XxxServiceImpl類是個上帝類,每每經過過程式業務邏輯實現。前幾行代碼作校驗,接下來作數據類型轉換,而後是業務處理邏輯的代碼,中間穿插着經過接口或者dao獲取更多的數據;拿到數據後,又是類型轉換代碼,而後接着一段業務邏輯代碼,最後可能還要落庫、發佈消息等等。這樣的代碼參雜了太多不一樣的代碼,很是難以維護。

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

  1. 接口層負責對外暴露各類協議的接口好比http、tcp,轉換成應用服務能認識的協議。

  2. 核心的領域層不依賴其餘層,經過資源庫包下的接口定義作到依賴倒置,接口參數不能體現具體技術實現細節,領域模型裏的實現邏輯只依賴接口。這樣作到對領域邏輯的一層防腐。本層裏以聚合爲單位放置代碼,便於之後系統拆分,以聚合爲單位。

  3. 應用層定義應用服務,一個接口對應業務場景的一個用例。此外應用層還能夠處理橫切面事務好比啓動數據庫事務。

  4. 基礎設施層完成資源庫的實際實現,以及領域層定義的其餘接口的實現如對外部服務的訪問,領域事件發佈到消息隊列中間件等。

  5. 分層架構還定義了每層的項目包結構,不一樣的領域概念和數據對象相應的命名規範。

圖片

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

1

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

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

2

repository和dao的區別?

聚合設計要儘可能小,若是一個實體不是根實體,但同時須要被外界直接訪問到,那麼這個實體不該該在這個聚合中,應該獨立成新的聚合。

3

領域事件的發佈應該在領域層仍是應用層?

只要不會破壞各層的依賴順序,在哪發佈都行。取決於領域事件定義在哪層?通常推薦定義在領域層的聚合內。固然即使在應用層發佈事件也不會破壞依賴方向。所以聚合、領域服務、應用服務均可以發佈事件。

3.6  代碼示例

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

總之,儘量作到了能根據需求文檔裏的業務邏輯很快找到代碼所在之處,讓不一樣的代碼待在應該待的分層和包下面。團隊成員開玩笑說,如今開發業務代碼就像在作填空題,簡單直白。

圖片

3.7  收益

目前百度愛番番的新服務默認都會在符合DDD架構的骨架代碼基礎上開發,存量的核心模塊也進行過DDD改造。全面實施DDD後產研團隊目標更對齊,協做效率更高,收穫了不少收益,包括但不限於如下幾點:

  1. 產研團隊協同成本下降,領域知識獲得積累和沉澱。統一語言的使用和維護極大提升了你們對齊的成本。

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

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

  4. 接口數量精簡、可控。因爲業務代碼聚焦領域模型,邏輯內聚,複用性高,急劇減小了接口數量,下降接口維護成本。

  5. 經過預約義好的腳手架建立符合DDD規範的代碼骨架,提升了新服務開發的效率。

  6. 代碼可讀性高,不是代碼做者也能快速定位到代碼位置,代碼設計可以獲得傳承,可維護性也提升了。

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

3.8  實踐總結

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

圖片


4  結語:異曲同工、沒有銀彈


DDD一方面使用分而治之的思想,引入劃分領域、限界上下文、模塊分層、劃分聚合在不一樣層次、不一樣粒度來下降問題的複雜度。另外一方主張聚焦領域邏輯,經過不一樣手段來減小業務和技術的耦合。所以DDD只是大部分軟件設計思想一種,軟件設計的本質都是爲了高內聚低耦合。可是DDD並非萬能的,不是全部業務開發場景都適合用DDD。有些簡單業務場景不使用DDD反而更恰當。由於DDD有較高的學習門檻,須要整個團隊造成統一認識和協同,須要相應的編碼規範和架構落地。所以學習和落地DDD時要時刻記住本身的出發點是爲了應對如今或者未來的複雜業務領域而來。沒必要太拘泥於某些點是否遵照了DDD原則,若是以爲用了DDD會比沒有用好一點點,也值得邁出這一步。

愛番番產研團隊始終秉持「以客戶爲中心」的理念,運用DDD設計思想構建統一的業務模型,實現業務功能的複用和融合。隨着愛番番業務的發展,咱們相信DDD帶來的收益會更大。從此咱們會從產品、技術、流程和組織方面持續關注能有效解決軟件工程複雜性問題的方法。

本期做者|飛邪

在百度愛番番主要負責銷售域和連通域的技術,長期關注技術團隊如何高效服務產品團隊等研發效能話題,擅長ToB企業級應用的規劃和落地。

招聘信息

不管你是後端,前端 ,大數據仍是算法,這裏有若干職位在等你,歡迎投遞簡歷, 愛番番業務部期待你的加入!

閱讀原文:領域驅動設計(DDD)在百度愛番番的實踐

更多幹貨、內推福利,歡迎關注同名公衆號「百度Geek說」~

相關文章
相關標籤/搜索