天天看點

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

領域驅動設計(Domain-Driven Design,以下簡稱DDD)思潮的形成要追述到30幾年前,17年前,Eirc Evans定義了領域驅動設計的概念。DDD一直為傳統行業的軟體工程師提供軟體設計的方法論,但是在網際網路行業卻使用很少。直到近幾年,DDD在網際網路行業被重新認識,火了起來。究其原因有兩點:

  • 網際網路的行業的業務越來越複雜,面臨傳統行業軟體同樣的問題;
  • 微服務的流行帶火了DDD,來解決微服務拆分問題。

本文主要對第一點“解決軟體複雜性之道”進行講解。

價值

詳細講解之前,我們先給出DDD為打賞業務帶來的價值。

會員業務部門在打賞業務進行了DDD實踐後,效率有顯著提升:

  • 新需求接入開發成本節約20%;
  • 更換底層中間件開發成本節約20%;
  • 項目熟悉成本節約30%(對DDD有基本了解為前提);
  • 單測開發成本指數級降低;
  • 上線風險、成本降低。

了解了DDD流行的背景及業務價值後,下面我們對DDD是什麼、有哪些優勢、項目中如何實踐,以及幾個關鍵問題進行叙述。

領域驅動設計是什麼

讨論領域驅動設計是什麼之前,我們先看下面一段代碼:

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

這是一個打賞接口定義,單看這個接口是沒有問題的,使用者基于活動,選擇明星,選擇道具進行打賞。

業務邏輯上沒問題,但我們會發現一些代碼的壞味道。

  • 代碼編譯後方法的參數名會丢失。
領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

在編碼時如果将明星和道具的參數順序傳錯一樣可以通過編譯,隻有運作時才會報業務錯誤。當然這種問題發現成本不是很高,如果在編譯時發現會規避一些風險,降低排查成本。上面方法參數的幾個code可以唯一辨別出實體,但卻丢失了原有實體的實際業務領域意義,為某個明星打賞某種道具。

打賞業務邏輯摻雜了很多與業務核心邏輯無關的前置校驗邏輯,影響代碼可讀性。全部邏輯堆疊在一個方法中,增加了測試用例編寫複雜度。

對于以上代碼,問題的根本原因是我們對業務的領域沒有明确的劃分,隻是實作了一個操作流程,是需求的直接實作,缺乏領域抽象,方法的參數定義缺少業務領域含義。對于活動校驗本質是活動的一些屬性判斷,活動是否有效是活動自身的屬性決定的。可以抽象出活動校驗類ActivityValidate,或在實體中增加validate方法,還可以更近一步,将校驗邏輯直接放在活動的構造方法中,這樣既達到了校驗的目的,也避免了漏校驗。對于單測的編寫,如果我們能将一個大邏輯拆分為多邏輯單元,無疑會大大減少用例數量。優化後的代碼如下:

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

對于活動的校驗,采用構造函數校驗,是以打賞方法中無需再校驗活動。活動的校驗放到前置構造函數,減少了測試用例數量。

回到最初的問題,什麼是領域驅動設計?

1、領域驅動設計基于領域模組化而非資料模組化

上面例子中,代碼重構前activity實體隻有基本的屬性和get/set方法,即“失血模型”,進而導緻activity這個領域對象退化為資料對象,隻用作orm元件的crud,失血模型在項目代碼中随處可見。究其原因,跟對象-關系映射(ORM,比如hibernate)持久化機制的流行是有直接關系的,使用ORM将每個類映射到一張資料表,通過實體對象完成crud,久而久之實體成為了orm架構的專用名詞,即喪失了領域能力。進行項目設計時,我們應該從業務領域角度出發思考問題,而不應該從資料庫角度,我們将在戰略設計部分詳細讨論。

2、滿足六邊形架構設計

六邊形架構在後文進行詳細介紹,洋蔥架構、幹淨架構與六邊形架構類似。

滿足以上兩點,并對DDD的一些概念進行映射實踐,那麼你的系統已經符合DDD了。總結,DDD不是一套全新的特殊架構,是任何項目代碼經過重構,滿足高可維護性、高可擴充性、高可測試性、代碼結構清晰之後必将達到的終點。

DDD打賞業務實踐

1、打賞業務簡介

  • 觀看視訊時,選擇明星、禮物進行打賞;
  • 打賞後螢幕有氣泡提示;
  • 打賞資料在排行榜進行顯示;
  • 累計一定的打賞獲得某種獎勵。

2、戰略設計

提到戰略設計,不得不提的是戰略設計的幾個核心概念:領域、子域、限界上下文、架構分層。

領域:從廣義上講,領域即是一個組織所做的事情以及其中包含的一切。每個公司或組織都有它自己的業務範圍和做事方式,這個業務範圍以及其在其中所進行的活動便是領域。當你為某個公司開發軟體時,你面對的便是這個公司的領域。這個領域對于你來說應該是明确的,因為你在這個領域中工作。

對于打賞這種業務,打賞本身便是領域,即打賞領域。無論你的打賞對象是一位主播、一部電影或者一篇博文,又無論你的打賞道具是RMB、虛拟币、火箭等等,打賞都是這個領域的核心。

子域:對于領域模型包含“領域”這個詞,我們可能會認為整個業務系統建立一個單一的、内聚的、功能全能式的模型。然後這并不是我們使用DDD的目标。正好相反,在DDD中,一個領域被分成若幹小的域,這些若幹的小的域即子域。事實上,在開發一個領域模型時,我們關注的通常隻是這個業務系統的某個方面,對領域的拆分将有助于我們成功。

限界上下文:一個由顯示邊界限定的特殊職責。領域模型存在于邊界之内,在邊界内,每一個模型概念,包括它的屬性和操作,都具有特殊的含義。

打賞系統搭建之初,需求比較簡單,随着業務發展,需求越來越複雜,疊代及領域拆分過程如下:

  • 營運及産品需求非常簡單,隻要實作免費打賞并在界面實作打賞氣泡,基于此,隻有一個領域;
  • 經過一階段的試水,活動效果很好,需要能同時支援多場打賞活動,增加活動支撐子域;
  • 需求方又有了新想法,使用者完成一定打賞後給使用者發放一些獎品,引入獎勵子域;
  • 為了提升使用者參與感,增加排行功能,引入排行子域。

最終領域劃分如下圖:

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包
  • 打賞核心子域:完成打賞操作。
  • 通知子域:實作界面氣泡通知能力。
  • 獎勵子域:獎勵政策比對,獎勵發放。
  • 排行子域:完成排行功能。
  • 活動子域:活動、明星、道具管理。
  • 使用者子域:完成使用者查詢、校驗等通用能力。

領域的拆分過程并沒有上面描述的那麼順利,經曆了很多推翻重來的過程,正是經曆了這些過程,我們對領域的了解才能更深入,更符合領域模組化。

架構分層:分層架構的一個重要原則是——每層隻能與位于其下方的層發生聚合。

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

為了實作接口的定義與實作解耦,接口定義在領域層,實作定義在基礎設施層。但這樣違背了由頂至底的單項依賴原則。為了解決這個問題,我們此處采用依賴倒置原則,依賴倒置原則内容——高層子產品不應該依賴于低層子產品,兩者都應該依賴于抽象。抽象不應該依賴于細節,細節應該依賴于抽象。根據此原則,結構調整如下:

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

我們将基礎設施層放在所有層的最上方,這樣它可以實作所有其它層定義的接口。

當我們在分層架構中采用依賴倒置原則時,我們可能會發現,事實上已經不存在分層的概念了。無論是高層還是底層,它們都隻依賴于抽象,好像把整個分層推平了一樣。推平之後,客戶通過“平等”的方式與系統互動。加入新的客戶也隻是不同的輸入、輸出,以及不同的展現形式而已,這既是我們即将了解的另一個架構,六邊形架構。

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

六邊形架構

在我們的代碼中,有很多直接的外部依賴和實作細節。如mybatis的mapper類、httpclient注入、rocketmq的監聽、緩存的直接操作等等。這樣的實作有兩個比較明顯問題,一是當底層更換基礎元件時對業務邏輯有直接影響,更換代碼改動量及測試範圍大大增加。二是不利于功能的複用,如果其他業務有類似邏輯,做不到直接移植複用。

2005年Alistair Cockburn提出了六邊形架構,又被稱為端口和擴充卡架構。觀察上圖我們發現,對于核心的應用程式和領域模型來說,其他的底層依賴或實作都可以抽象為輸入和輸出兩類。組織關系變為了一個二維的内外關系,而不是上下結構。每個io與應用程式之前均有擴充卡完成隔離工作,每個最外圍的邊都是一個端口。基于六邊形架構設計的系統是DDD追求的最終形态。六邊形架構的實踐在“DDD的優勢”部分進行講解。

先給出基于六邊形架構實踐後,項目子產品結構:

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

子產品

說明

Admin-api

配置背景相關

api

對外使用者接口

application

應用

domain

領域

infrastructure

基礎設施

query

查詢子產品

task

與使用的中間件相關,可忽略

worker

處理事件消息子產品

common

基礎包

3、戰術設計

經過戰略設計後,領域已經有了清晰的邊界,下面我們聊下戰術設計。首先對DDD的幾個基本概念進行業務映射。

實體:由屬性和行為組成,具有唯一辨別。

在設計系統時,我們趨向于将重點放在資料上,而不是領域上。對于DDD開發者來說也是如此,因為軟體開發中,資料庫依然占據着主導地位。首先考慮的是資料的屬性和關聯關系,而不是富有行為的領域概念。這樣做的結果是将資料模型直接反映在對象模型上,導緻實體隻包含get/set方法,這不是DDD的做法。

隻有get/set實體需配合service使用,内聚性、可維護性,以及複用遷移成本均明顯高于DDD的做法。

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包
領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

值對象:沒有唯一辨別,具有可度量或可描述,并滿足不變性的對象。

我們應該盡量使用值對象來模組化而不是實體對象,因為相比實體,我們可以非常容易地對值對象進行建立、測試、使用、優化和維護。         

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包
領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

對于第一種實作,使用者必須知道需要同時使用amount和currency,并且應該知道如何使用這兩個屬性,原因在于這兩個屬性并沒有組成一個概念整體。對于PropName值對象的定義,可以帶來一些擴充性,如需要對道具名稱做大小寫轉換,此操作可以在PropName的内部實作,對外name的邏輯洩漏到Prop中。

領域服務:領域服務表示一個無狀态的操作,它用于實作特定于某個領域的任務。

當某個操作不适合放在聚合和值對象上時,最好的方式便是使用領域服務了。

例如“使用者認證”,一種方式是我們可以簡單地将認證操作放在實體上。對于這種設計,存在兩個問題。首先,使用者類需要知道某些認證細節,其次,這種方法也不能顯示的表達通用語言。這裡我們詢問的是一個User“是否被認證了”,而沒有表達出“認證”這個過程。在有可能的情況下,我們應該盡量使用模組化術語直接地表達出交流語言。

領域事件:領域專家所關心的發生在領域中的一些事件。我們通常将領域事件用于維護事件的一緻性,這樣可以消除兩階段送出(全局事務)。

聚合:聚合是一組相關對象的組合,作為一個整體被外界通路,聚合根是這個聚合的根節點。

聚合是一個非常重要的概念,核心領域往往都是用聚合來表達。其次,聚合在技術上也有非常高的價值,可以指導詳細設計。聚合由根實體、實體、值對象組成。

工廠:工廠提供一個建立對象的接口,該接口封裝了所有建立對象的複雜操作過程,同時,它并不需要客戶去引用實際被建立的對象。

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

對于上面例子中方法參入為什麼傳入資源庫對象,我們将在下面六邊形架構部分講解。

DDD的優勢

應用DDD的系統符合六邊形架構,我們實作了以下目标:

  • 獨立于架構:架構不應該依賴某個外部的庫或者架構,不應該被架構的結構所束縛;
  • 獨立于UI:前台展示的樣式可能會随時發生變化,但是底層架構不應該随之而變化;
  • 獨立于底層資料源:軟體架構不應該因為不同的底層資料存儲而産生巨大改變;
  • 獨立于外部依賴:無論外部依賴如何變更、更新,業務的核心邏輯不應随之而大幅變化。

實作以上幾個目标,六邊形架構(洋蔥架構、幹淨架構與此類似)是個不錯的選擇,下面結合打賞具體業務實作,講解如何實作以上目标。

先給出項目某個子產品的代碼包結構:

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

資源庫:對于資源庫,我們的實踐是資源庫作為業務與資料的隔離層,屏蔽底層資料表細節,同時完成PO與DO的轉化。DO與PO的轉化帶來的好處是領域層不會直接依賴底層實作,便于後續更換底層實作或功能遷移。資源庫接口定義在領域層,接口實作在基礎設施層。

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包
領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

RPC:RPC部分的結構拆分與資源庫類似,差別是以領域服務的存在。接口定義放在領域層,具體實作在基礎設施層。

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包
領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

在遵循六邊形架構的大原則下,其他邊的拆分也變得清晰簡單了,此處不再贅述。

幾個關鍵問題

1、事務

在上文中“聚合”章節,我們描述了事務,一個事務内隻操作一個聚合執行個體。如果發現一次事務内邏輯過多,可以考慮剝離出獨立的聚合,采用最終一緻性。基于這個基礎,最适合聲明事務的層是應用層。

2、查詢

CQRS 在 DDD 中是一種常常被提及的模式,它的用途在于将領域模型與查詢功能進行分離,讓一些複雜的查詢擺脫領域模型的限制,以更為簡單的 DTO 形式展現查詢結果。同時分離了不同的資料存儲結構,讓開發者按照查詢的功能與要求更加自由的選擇資料存儲引擎,CQRS的具體實踐可以自行查找資料。

3、架構無關

基于六邊形架構設計,已經做到了與底層實作、架構、中間件無關。但還有一個最大的架構依賴spring,我們的做法是領域内使用的spring bean通過傳參方式,實作領域層架構解耦。

4、成本

成本是我們實踐DDD時需要考慮的一個很重要的問題,學習成本、改造成本、相容成本等等都是需要特别關注的。在動手實踐之前,建議優先評估好成本。

結束語

DDD不是一套全新的特殊架構,是應對軟體複雜性的一套方法論。是面向領域模組化,基于六邊形架構,項目代碼經過重構,滿足高可維護性、高可擴充性、高可測試性、代碼結構清晰之後必将達到的終點。

DDD缺少權威性的實踐指導和代碼限制,是以應用過程中會碰到很多問題,愛奇藝會員團隊通過幾個月的實踐積累了一定的經驗,歡迎交流。

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包

參考閱讀

  • Redis 日志篇:無畏當機實作高可用的殺手锏
  • 喜馬拉雅自研網關架構演進過程
  • 如何從0到1建構穩定、高性能Redis叢集?
  • 深度剖析——傳統架構的雲原生改造之路
  • 領域驅動設計架構Axon實踐

技術原創及架構實踐文章,歡迎通過公衆号菜單「聯系我們」進行投稿。

高可用架構

改變網際網路的建構方式

領域驅動設計(DDD)在愛奇藝打賞業務的實踐子產品說明Admin-api配置背景相關api對外使用者接口application應用domain領域infrastructure基礎設施query查詢子產品task與使用的中間件相關,可忽略worker處理事件消息子產品common基礎包