領域驅動設計基礎
1. 什麼是領域(Domain)
我們所做的軟體系統的目的都是來解決一系列問題,例如做一個電商系統來線上銷售自己企業的產品;做一個灰度釋出平臺來提升服務的質量和穩定性。任何一個系統都會屬於某個特定的領域,例如:
- 論壇是一個領域:要做一個論壇,那這個論壇的核心業務是確定的:比如使用者發帖、回帖等核心基本功能;
- 電商系統是一個領域:只要是電商領域的系統,那核心業務就是:商品瀏覽、購物車、下單、減庫存、付款交易等核心環節;
同一個領域的系統都具有相同的核心業務,因為他們要解決的問題的本質是類似的。因此可以推斷:一個領域本質上可以理解為一個 問題域 。只要確定了系統所屬的領域,那麼這個系統的核心業務,即要解決的關鍵問題就基本確定了。通常我們說,要成為一個領域的專家,必須要在這個領域深入研究很多年才行,只有這樣才會遇到非常多的該領域的問題,積累了豐富的經驗。
2.界限上下文(Bounded Context)
通常來說,一個領域有且只有一個核心問題,我們稱之為該領域的『核心子域』。在核心子域、通用子域、支撐子域梳理的同時,會定義出子域中的『限界上下文』及其關係,用它來 闡述子域之間的關係 。界限上下文可以簡單理解成一個子系統或元件模組。
例如:下圖是對酒店管理的子域和界限上下文的梳理:
3. 領域模型(Domain Model)
領域驅動設計(Domain-Driven Design)分為兩個階段:
- 以一種領域專家、設計人員、開發人員都能理解的通用語言作為相互交流的工具,在交流的過程中發現領域概念,然後將這些概念設計成一個領域模型;
- 由領域模型驅動軟體設計,用程式碼來實現該領域模型;
由此可見,領域驅動設計的核心是建立正確的領域模型。領域模型具有以下特點:
- 對具有某個邊界的領域的一個抽象,反映了領域內使用者 業務需求的本質 。它屬於『解決問題空間』。領域模型是有邊界的,只反應了我們在領域內所關注的部分,包括 實體概念(如:貨物,書本,應聘記錄,地址等),以及 過程概念(如:資金轉賬等);
- 提高軟體的 可維護性,業務可理解性以及可重用性。領域模型確保了我們的軟體的業務邏輯都在一個模型中,幫助開發人員相對平滑地將領域知識轉化為軟體構造;
- 貫穿軟體 分析、設計、開發 的整個過程。領域專家、設計人員、開發人員面向同一個模型進行交流,彼此共享知識與資訊,所以可以防止需求走樣,讓軟體開發人員做出來的軟體真正滿足需求;要建立正確的領域模型並不簡單,需要領域專家、設計、開發人員積極溝通共同努力,然後才能使大家對領域的認識不斷深入,從而不斷細化和完善領域模型;
- 為了讓領域模型看的見,使用的常用表達領域模型的方式:圖、程式碼或文字;
- 重要性:領域模型是整個軟體的核心,是軟體中最有價值和最具競爭力的部分;設計足夠精良且符合業務需求的領域模型能夠更快速的響應需求變化;
4. 領域通用語言
由軟體專家和領域專家合作開發一個領域的模型是有必要的。開發過程中, 開發人員以類、演算法、設計模式、架構等進行思考與交流。但領域專家對此一無所知,他們對技術上的術語沒有太多概念,只瞭解特有的領域專業技能,例如:在空中交通監控樣例中,領域專家知道飛機、路線、海拔、經度、緯度,他們有自己的術語來討論這些事情。軟體專家和領域專家交流過程中,需要做翻譯才能讓對方理解這些概念。
領域驅動設計的一個核心原則是使用一種基於模型的語言。使用模型作為語言的核心骨架,要求團隊在進行所有的交流是都使用一致的語言,在程式碼中也是這樣,這種語言被稱為『通用語言』。
5.建模思考的問題:使用者需求
『使用者需求』不能等同於『使用者』,捕捉『使用者心中的模型』也不能等同於『以使用者為核心設計領域模型』。設計領域模型時不能以使用者為出發點去思考問題,不能老想著使用者會對系統做什麼;而應該從一個客觀的角度,根據使用者需求挖掘出領域內的相關事物,思考這些事物的本質關聯及其變化規律作為出發點去思考問題。
領域模型是 排除了人之外的客觀世界模型 ,包含了人所扮演的參與者角色。但是一般情況下不要讓參與者角色在領域模型中佔據主要位置,否則各個系統的領域模型將變得沒有差別,因為軟體系統就是一個人機互動的系統,都是以人為主的活動記錄或跟蹤。例如:
- 論壇中如果以人為主導,那麼領域模型就是:人發帖,人回帖,人結貼,等等;
- 貨物託運系統中如果以人為主導,就變成了:託運人託運貨物,收貨人收貨物,付款人付款,等等;
以一個貨物運輸系統為例子簡單說明一下。在使用者需求相對明朗之後,這樣描述領域模型:
- 一個Cargo(貨物)涉及多個Customer(客戶,如託運人、收貨人、付款人),每個Customer承擔不同的角色;
- Cargo的運送目標已指定,即Cargo有一個運送目標;
- 由一系列滿足Specification(規格)的Carrier Movement(運輸動作)來完成運輸目標;
以上描述沒有從使用者的角度去描述領域模型,而是以領域內的相關事物為出發點,考慮這些事物的本質關聯及其變化規律的:
- 以貨物為中心,把客戶看成是貨物在某個場景中可能會涉及到的關聯角色,如貨物會涉及到託運人、收貨人、付款人;
- 貨物有一個確定的目標,貨物會經過一系列的運輸動作到達目的地。
以使用者為中心來思考領域模型的思維只是停留在需求的表面,而沒有挖掘出真正的需求的本質。領域建模時需要努力挖掘使用者需求的本質,這樣才能真正實現使用者需求。
6. 經典分層架構
使用者介面/展示層:1)請求應用層獲取使用者所需的展示資料;2)傳送命令給應用層執行使用者的命令
應用層:薄薄的一層,定義軟體要完成的任務。對外為展示層提供各種應用功能,對內呼叫領域層(領域物件或領域服務)完成各種業務邏輯。應用層不包含業務邏輯
領域層:表達業務概念、業務狀態資訊及業務規則,是業務軟體的核心
基礎設施層:為其他層提供通用的技術能力,提供了層間通訊;為領域層提供持久化機制。
7. 使用的模式
7.1. 總覽圖
7.2. 關聯的設計
關聯在領域建模的過程中非常重要,關聯的設計可以遵循如下的一些原則:
- 關聯 儘量少。物件之間複雜的關聯容易形成物件的關係網,對於理解和維護單個物件很不利,同時也很難劃分物件與物件之間的邊界;另外,減少關聯有助於簡化物件之間的遍歷;
- 關聯儘量保持 單向 的關聯;
- 在建立關聯時,需要挖掘是否存在關聯的 限制條件 。如果存在,那麼最好把限制條件加到關聯上,往往這樣的限制條件能將關聯化繁為簡,即將多對多簡化為1對多,或將1對多簡化為1對1;
7.3. 實體(Entity)
實體就是領域中需要 唯一標識 的領域概念。因為我們有時需要區分是哪個實體:有兩個實體,如果唯一標識不一樣,那麼即便實體的其他所有屬性都一樣,也認為他們是兩個不同的實體。
不應該給實體定義太多的屬性或行為,而應該尋找關聯,將屬性或行為轉移到其他關聯的實體或值物件上。比如:Customer 實體,有一些地址資訊,由於地址資訊是一個完整的有業務含義的概念,所以我們可以定義一個 Address 物件,然後把 Customer 的地址相關的資訊轉移到 Address 物件上。如果沒有 Address 物件,而把這些地址資訊直接放在 Customer 物件上,然後對於一些其他的類似Address的資訊也都直接放在Customer 上,會導致 Customer 物件很混亂,結構不清晰,最終導致它難以維護和理解。
7.4. 值物件(Value Object)
並不是每一個事物都必須有一個唯一標識。就以上面的地址物件 Address 為例,如果兩個 Customer 的地址資訊是一樣的,我們就會認為這兩個 Customer 的地址是同一個。用程式的方式來表達就是:如果兩個物件所有屬性的值都相同,我們會認為它們是同一個物件,那麼就可以把這種物件設計為值物件。
值物件的特徵:
- 值物件 沒有唯一標識 ,這是它和實體的最大不同。值物件在判斷是否是同一個物件時是通過它們的所有屬性是否相同,如果相同則認為是同一個值物件。在區分是否是同一個實體時,只看實體的唯一標識是否相同,而不管實體的屬性是否相同。
- 值物件是 不可變 的,即所有屬性都是隻讀的,所以可以被安全的共享。
應該給值物件設計的儘量簡單,不要讓它引用很多其他的物件。值物件只是一個值,類似(int a = 3)中的『3』,只不過是用物件來表示。值物件雖然是隻讀的,是一個完整的不可分割的整體,但是可以被整個替換掉:類似(a = 4)把a的值由『3』替換為為『4』,當修改 Customer 的 Address 物件引用時,不是通過 Customer.Address.Street 這樣的方式來修改屬性,可以這樣做:Customer.Address = new Address(…)
7.5. 領域服務(Domain Service)
領域中的一些概念不太適合建模為物件(實體物件或值物件),因為它們本質上就是一些操作、動作,而不是事物。這些操作往往需要 協調多個領域物件。如果強行將這些操作職責分配給任何一個物件,則被分配的物件就是承擔一些不該承擔的職責,從而會導致物件的職責不明確很混亂。DDD認為領域服務模式是一個很自然的正規化用來對應這種跨多個物件的操作。一般的領域物件都是有狀態和行為的,而領域服務沒有狀態只有行為。
領域服務還有一個很重要的功能就是可以避免領域邏輯洩露到應用層。因為如果沒有領域服務,那麼應用層會直接呼叫領域物件完成本該是屬於領域服務該做的操作,需要了解每個領域物件的業務功能,以及它可能會與哪些其他領域物件互動等一系列領域知識。這樣一來,領域層可能會把一部分領域知識洩露到應用層。對於應用層來說,通過呼叫領域服務提供的簡單易懂且意義明確的介面肯定也要比直接操縱領域物件容易的多。
說到領域服務,還需要提一下軟體中一般有三種服務:應用層服務、領域服務、基礎服務。從以下的例子中可以清晰的看出每種服務的職責:
應用層服務
- 獲取輸入(如一個XML請求)
- 傳送訊息給領域層服務,要求其實現轉帳的業務邏輯
- 領域層服務處理成功,則呼叫基礎層服務傳送Email通知
領域層服務
- 獲取源帳號和目標帳號,分別通知源帳號和目標帳號進行扣除金額和增加金額的操作
- 提供返回結果給應用層
基礎層服務
- 按照應用層的請求,傳送Email通知
7.6. 聚合及聚合根(Aggregate,Aggregate Root)
聚合定義了一組具有 內聚關係 的相關物件的集合,以及物件之間清晰的所屬關係和邊界,避免了錯綜複雜的難以維護的物件關係網的形成。我們把聚合看作是一個修改資料的單元。
聚合有以下特點:
- 每個聚合有一個根和一個邊界:根是聚合內的某個實體;邊界定義了一個聚合內部有哪些實體或值物件;
- 聚合根是外部可以保持對聚合引用的唯一元素,負責與外部其他物件打交道並維護自己內部的業務規則。聚合內部的物件之間可以相互引用,但是聚合外部如果要訪問聚合內部的物件時,必須通過聚合根開始導航,絕對不能繞過聚合根直接訪問聚合內的物件;
- 聚合內除根以外的其他實體的唯一標識都是本地標識,也就是隻要在聚合內部保持唯一即可,因為它們總是從屬於這個聚合的;
- 聚合內部的物件可以保持對其他聚合根的引用;
- 刪除一個聚合根時必須同時刪除該聚合內的所有相關物件,因為他們都同屬於一個聚合,是一個完整的概念;
- 基於聚合的以上概念,我們可以推論出從資料庫查詢時的單元也是以聚合為一個單元,不能直接查詢聚合內部的某個非根的物件;
如何識別聚合:
可以從業務的角度分析哪些物件它們的關係是內聚的,可看成一個整體來考慮的,然後這些物件可以放在一個聚合內。關係內聚是指這些物件之間必須保持一個固定規則,固定規則是指在資料變化時必須保持不變的一致性規則。當修改一個聚合時,必須在 事務級別
確保整個聚合內的所有物件滿足這個固定規則。聚合儘量不要太大,否則可能帶來一定的效能問題。通常在大部分領域模型中,有70%的聚合通常只有一個實體,即聚合根,該實體內部沒有包含其他實體,只包含一些值物件;另外30%的聚合中,基本上也只包含兩到三個實體。
如何識別聚合根:
如果一個聚合只有一個實體,那麼這個實體就是聚合根;如果有多個實體,那麼我們可以思考聚合內哪個物件有獨立存在的意義並且可以和外部直接進行互動。
7.7. 工廠(Factory)
DDD中的工廠也是一種體現 封裝思想 的模式。DDD中引入工廠模式的原因是:有時建立一個領域物件是一件比較複雜的事情,不僅僅是簡單的new操作。工廠是用來封裝建立一個複雜物件尤其是聚合時所需的知識,將建立物件的細節(如何例項化物件,然後做哪些初始化操作)隱藏起來。
客戶傳遞給工廠一些簡單的引數,如果引數符合業務規則,則工廠可以在內部創建出一個相應的領域物件返回給客戶;但是如果引數無效,應該丟擲異常,以確保不會創建出一個錯誤的物件。當然也並不總是需要通過工廠來建立物件,事實上大部分情況下領域物件的建立都不會太複雜,只需要簡單的使用建構函式就可以了。隱藏建立物件的好處:可以不讓領域層的業務邏輯洩露到應用層,同時也減輕了應用層的負擔,它只需要簡單的呼叫領域工廠創建出期望的物件即可。
7.8. 倉儲(Repository)
倉儲被設計出來的原因:領域模型中的物件自從建立後不會一直留在記憶體活動,當它不活動時會被持久化到DB中,當需要的時候會重建該物件。所以,重建物件是一個和DB打交道的過程,需要提供一種機制,提供類似集合的介面來幫助我們 管理物件。
倉儲裡存放的物件一定是聚合,因為之前提到的領域模型是以聚合的概念來劃分邊界的。我們 只對聚合設計倉儲 ,把整個聚合看成一個整體,要麼一起取出來,要麼一起被刪除,不會單獨對某個聚合內的子物件進行單獨查詢和更新。倉儲還有一個重要的特徵就是分為倉儲定義部分和倉儲實現部分,在領域模型中定義倉儲的介面,而在基礎設施層實現具體的倉儲。
8.設計領域模型時一般步驟
- 根據需求建立初步的領域模型,識別明顯的領域概念和之間的關聯(1:1, 1:n的關係),用文字精確沒有歧義的描述出每個領域概念的含義;
- 分析主要的軟體功能,識別主要的應用層的類,這樣有助於及早發現哪些是應用層的職責,哪些是領域層的職責;
- 進一步分析領域模型,識別出實體、值物件、領域服務;
- 分析關聯,通過對業務的深入分析和軟體設計原則及效能方面的權衡,明確關聯的方向,去掉一些不需要的關聯;
- 找出聚合邊界及聚合根,在分析過程中會出現難以清洗判斷的選擇問題,這就依賴平時分析經驗的積累了;
- 為聚合根配置倉儲,一般情況下為一個聚合分配一個倉儲,此時設計好倉儲的介面即可;
- 遍歷所有場景,確定設計的領域模型能有效解決業務需求;
- 考慮如何建立實體和值物件,是通過工廠還是建構函式;
- 重構模型,尋找模型中有疑問或蹩腳的地方,比如思考:聚合的設計是否正確,模型的效能等等;
領域建模是一個不斷重構,持續完善的過程,大家會在討論中將變化的部分反映到模型中,從而模型不斷細化並朝正確的方向走。