軟體系統是由軟體開發產生的用於解決業務領域或問題單元的可交付成果。 軟體設計可以幫助我們開發更強大的軟體系統。 因此,軟體設計是業務領域和軟體開發之間的橋梁。 DDD是軟體設計中的一種思想,旨在為大型和複雜的軟體提供設計思想和規範。 通過DDD思維,我們的業務架構、系統架構、部署架構、資料架構、工程架構等可以實現高擴充套件性、高維護性和高可測試性。
DDD本身是乙個想法,而不是乙個特定的技術,所以在實現和系統架構層面沒有限制。 由於市面上成熟的ORM框架(如Hibernate、MyBatis等),大多數軟體開發都是直接面向資料庫開發的。 傳統開發中的應用分層架構與DDD思維的分層架構非常相似。 因此,很多人在剛開始學習DDD時,就有一定的理解偏差,導致無法實現DDD的思想。
本文記錄了我對專案工程**重構DDD的學習、感悟和實踐經驗!
領域“元資料”的含義。 最主要的是解釋該領域的基本原理。 這也是使用DDD思想的基本標準。
exp:**號碼通常為區號+號碼的組合。 在實際業務中,會有很多業務需要**號。 例如,登入認證、導購配送等業務;我們需要對**號進行基本檢查;獲取區號等;在正常操作下,每個使用 **number** 的方法的入口處都會寫入大量這樣的檢查和判斷,雖然我們可以提取其校驗和,將區號程式碼獲取到 util 類中(其實大多數專案都是這樣做的),但這種方法處理的是標本而不是根本原因。 基於DDD的思想,可以發現這裡隱含著乙個概念:區號編碼。
基於 DDD 的思想,我們可以將 ** number 建立為具有獨立概念和行為的值物件:phonenumber,並將基本校驗和獲取編碼等無狀態行為封裝在 value 物件中。 這樣就無需在方法中編寫大量校驗和判斷。
exp:在銀行轉賬場景中,我們通常說賬戶 A 向賬戶 B 轉賬 1,000 元。 這裡的1000元其實有兩個含義,數字1000,貨幣美元。 但是我們通常忽略貨幣單位。 因此,在實現傳遞函式時不考慮單位。 一旦有國際轉移,它就會陷入很多困境。
基於DDD的思想,我們把貨幣作為乙個具有獨立概念和行為的價值物件:貨幣,這樣我們所說的貨幣就有了完整的概念。 通過這種方式,貨幣的隱含上下文可以是明確的,從而避免了目前未識別但將來可能爆發的錯誤。
exp:在跨境轉賬場景中,我們需要對匯率進行換算,我們可以將換算後的匯率封裝成乙個值物件。 通過封裝金額計算邏輯和各種檢查邏輯,整個方法極其簡單。
dp是阿里神提出的乙個概念;價值物件是 DDD 思想中的乙個概念。
經過研究,我個人認為dp是對值物件的進一步補充,賦予了它更完整的概念。 在值物件的基礎上增加了[不變性]、[可驗證性]和[獨立行為]。 當然,這也是乙個要求,[沒有***,所謂沒有***就是【狀態不變】。
傳統的MVC架構分為表示層、業務邏輯層和資料訪問層,更注重從表示層到資料訪問層的自上而下的互動。
基於DDD原理,工程架構分為應用層、領域層和基礎設施層。 將專案中的不同職能和職責劃分為不同的層次結構。 核心業務邏輯位於域級別。
根據 DDD,應用層負責協調使用者介面和領域層之間的互動。 可以通俗地認為它是領域服務的編排,它本身不包含任何業務邏輯。
域層負責實現核心業務的邏輯和規則。 根據 DDD,該層包含實體模組、值物件模組、事件和域服務。
基礎架構層不處理任何業務邏輯,僅包含基礎架構,通常包括資料庫、定時任務、MQ、南向閘道器和北向閘道器。
在實際的業務邏輯中,除了使用者介面層之外,還有其他外部系統會呼叫此服務,比如 xxljob、MQ,或者向外部系統提供 HTTP 或 RPC 介面。 因此,在實踐中,應用層應協調外部系統與領域層之間的互動。
根據標準的架構層次結構依賴關係,應用層依賴於領域層和基礎設施層。 由於對基礎架構層的依賴,應用層本身的可維護性和可測試性受到破壞。 因此,我們需要基於介面進行依賴反轉。
為了防止領域概念洩露,應用層需要進一步抽象為外部服務和內部服務,所有外部服務都必須由域層通過內部服務呼叫。 這樣可以防止域模型洩漏。
同樣,根據標準的架構層次結構依賴關係,領域層依賴於基礎設施層,但這也破壞了領域層本身的可維護性和可測試性。 因此,基於DDD中儲存庫的思想,我們對儲存庫層進行抽象,並通過介面實現依賴反轉。 讓域層不再依賴於基礎架構層。 這提高了領域層本身的可維護性和可測試性。
對於基礎設施層來說,它的主要作用是提供基礎設施能力,如資料庫、MQ、遠端服務呼叫等。 進一步的抽象表明它們是埠和介面卡。 通過埠實現與外部系統的互動,通過介面卡完成資料和概念的轉換。
隨著依賴關係的逆轉,奇蹟發生了。 基礎架構層成為最外層。
結合對應用層、領域層、基礎設施層的進一步理解,以及倒置的應用架構,我們可以得到乙個六邊形架構:
在實際專案中,除了上面提到的三層之外,通常還會使用一些實用程式類(JSON解析工具類、字串工具類等)。 實用程式類可用於所有級別。
從工具類的定位來看,它應該屬於基礎設施層,但基礎設施層屬於頂層,如果放在基礎設施層,那麼就會打破依賴順序。 因此,在邏輯劃分方面,我們可以將工具類歸類為基礎設施或通用域,在具體的工程結構中,我們可以將工具類放在乙個單獨的模組中。
在實際工程中,還有另一種型別的**是依賴於配置的。 從業務維度上可以分為業務配置和基礎設施配置。 因此,我們需要根據配置的型別將其放在相應的位置。 例如,為了靈活響應服務,我們通常會配置乙個動態交換機來動態調整業務的邏輯,並且這個服務交換機的配置應該放在領域層例如,資料庫的配置屬於基礎架構配置,這種配置應該放在基礎架構層。
我們團隊的角色是業務的基礎,其中包括一系列基礎的能力建設。 對於iaaS系統,基於六邊形架構實現了以下工程結構:
在 DDD 思想中,儲存庫表示儲存庫的概念,用於區分資料模型和領域模型。 它操作的物件是聚合根,因此它屬於域層。
儲存庫模式有兩個非常重要的功能:1.它與底層儲存解耦2.為貧血模型的求解提供了規範。
由於過去 ER 模型和主流 ORM 框架的發展,許多開發人員在與關聯式資料庫對映的層面上仍然有實體的概念。 這樣一來,實體只有空屬性,實體的業務邏輯分散在服務、util、helper、handler 等各個角落。 這種現象被稱為貧血模型現象。
如何判斷你的專案是否存在貧血模型現象?1.大量xxxdo或xxx:實體物件僅包含對映到資料庫表的屬性,沒有行為或行為數量較少
2、各種服務、控制器、util、helpers、handlers中的業務邏輯:實體的業務邏輯分散在不同的層次、不同的類、不同的方法中,在類似場景中重複很多。
無法保證實體物件的完整性和一致性:在貧血模型下,實體屬性的狀態和值只能由呼叫方來保證,但屬性的 get 和 set 是公開的,因此所有呼叫方都可以呼叫它們。 因此,無法保證物件的完整性和一致性。
很難找到操作實體物件的邊界:由於物件只有屬性,因此屬性的邊界值和呼叫範圍不受實體本身的控制,可以在任何地方呼叫,邊界值和範圍只能由呼叫方來保證。 如果實體的邊界值發生變化,則所有呼叫方都需要對其進行調整,這很容易導致錯誤。
對底層的強依賴:貧血模型下的實體和資料庫模型對映、協議等。 因此,如果底層發生變化,那麼上層邏輯就需要完全改變。 “軟體”變為“韌體”。
綜上所述:在貧血模型下,軟體的可維護性、可擴充套件性、可測試性都極差!
擴充套件性:軟體的可維護性=當底層基礎設施發生變化時,新修改的數量是多少(可維護性越少越好) 軟體的可伸縮性=新增或更改業務邏輯時,修改量是多少(可擴充套件性越小越好) 軟體的可測試性=每次TC執行的時長*新增或更改業務邏輯時生成的TC(持續時間越短, TC越少,可測試性越好)。
1.資料庫思維。
隨著 ER 和 ORM 框架的發展,大多數開發人員在剛開始(自學、訓練等)時認為實體是資料庫表對映。於是,乾脆從面向業務的開發變成了面向資料庫的開發,漸漸地認為軟體開發就是CRUD。
2.簡單。 儘管一些架構師或開發人員知道貧血模式不好,但公司需要快速推出產品才能占領市場。 結果,工期大大壓縮。 貧血模型恰好很簡單,在軟體的早期階段,業務邏輯可以快速實現。 這迫使開發人員“在你說話之前實現它”。 這種現象也是行業內普遍存在的現象。
3.劇本思維。
有些開發者有一定的抽象思維,把一些常見的**寫成util、helper、handler等類。 但寫**還是劇本思維。 例如,在某個方法中,先做乙個字段校驗,然後做乙個物件轉換,然後呼叫遠端服務,再做乙個物件轉換,......遠端服務返回的結果最後,呼叫 DAO 類的方法儲存物件。 這種**在許多專案中太常見了。
基於這些因素,貧血模型很難消除。
這些因素的根本原因是什麼?根本原因是大多數開發人員混淆了資料模型和領域模型的概念。
資料模型:資料模型解決了資料如何持久化和傳輸的問題
Domin模型:域是指乙個獨立的業務領域或問題空間,域模型是為解決這個業務領域或問題空間而設計的模型這是乙個已解決的業務領域問題。
在 DDD 中,儲存庫是乙個用於區分資料模型和域模型的概念。
使用儲存庫,資料模型和域模型都可以完成它們的工作。 模型之間的轉換是通過彙編器和轉換器執行的。
在**中,動態轉換對映與靜態轉換對映。雖然 Assembler Converter 是乙個非常容易使用的物件,但是當業務複雜時,手寫 Assembler Converter 是一件耗時且容易出錯的事情,因此業界會出現各種各樣的 Bean 對映解決方案,這些解決方案本質上分為動態對映和靜態對映。
動態對映方案包括比較原始 beanutilsCopyProperties Dozer 的核心是通過 XML 進行配置,它基於執行時的反射動態分配值。 動態方案的缺點在於反射呼叫次數多、效能差、記憶體占用大,不適合特別高併發的應用場景。 但是,beanutils 等複製工具隱藏了內部複製過程,容易造成 bug,難以排查。
Mapstruct 在編譯時通過註解靜態生成對映,最終編譯的對映在效能上與手寫的完全一致,並具有強大的註解等能力。 將節省大量成本。
1. 介面名稱的命名約定。
不要使用底層儲存的名稱(插入、更新、新增、刪除、查詢等)作為儲存庫中的介面名稱,而是嘗試使用具有業務意義的名稱。 例如,s**e、remove、find 等。
2、介面引數規範。
儲存庫操作的物件是聚合根。 因此,您只能操作聚合根或實體。 這樣可以遮蔽底層資料模型,防止資料模型滲透到領域層。
大多數 DDD 體系結構的核心是實體類,它包含域的狀態和狀態的直接操作。 實體最重要的設計原則是保證實體的不變性,即保證無論外部做什麼,實體的內部屬性都不會相互衝突,狀態不一致。
構造函式引數應包含所有必需的屬性,或者在建構函式中具有合理的預設值。
由於建立中的一致性原則,構造實體的方法可能很複雜,因此您可以使用工廠模式來快速構造新實體。 降低呼叫者的複雜性。
不一致的最常見原因之一是實體公開了 public 的 setter 方法,尤其是在單個 set 引數可能導致狀態不一致的情況下。 如果需要更改狀態,請嘗試對方法名稱進行語義化。
通常主實體將包含子實體,然後主實體需要充當聚合根,即
子實體不能單獨存在,而只能通過聚合根來獲取。 任何外部物件都不能直接保留子實體的引用,子實體沒有獨立的倉庫,不能單獨儲存和檢索,必須通過聚合根的倉庫進行例項化,子實體可以獨立修改自己的狀態,但多個子實體之間的狀態一致性需要由聚合根來保證。
exp:電商領域聚合的常見案例,如主訂單和子訂單模型、產品SKU模型、跨子訂單折扣、跨店鋪折扣模型等。
實體的原則是高內聚、低耦合,即實體類不能直接依賴外部實體或服務。
對外部物件的依賴性直接導致實體未進行單一測試;並且實體不能保證對外部實體的更改不會影響該實體的一致性和正確性。
適當地依靠外部方式僅儲存外部實體的 id:在這裡,我再次強烈建議使用強型別 id 物件,而不是長型別 ID。 強型別 id 物件不僅包含自驗證**,以保證 id 值的正確性,還確保各種輸入引數不會因引數順序變化而出現 bug。
對於“no ***”的外部依賴,傳入方法入口引數的方法。 例如,上面的 equip(weapon,equipmentservice) 方法。
這個原則更多的是保證可讀性和可理解性的原則,即任何實體的行為都不能“直接”,即直接修改其他實體類。 這樣做的好處是,當你閱讀它時,不會有任何驚喜。
合規性的另乙個原因是降低未知更改的風險。 對系統中實體物件的所有更改都應該是預期的,如果可以隨意直接從外部修改實體,則會增加錯誤的風險。
當乙個業務邏輯需要使用多個域物件作為輸入,而輸出結果是乙個值物件時,就意味著需要使用域服務。
這種領域物件主要面向單個實體物件的變化,但涉及多個領域物件或外部依賴的一些規則。
在此型別中,實體應通過方法引數傳入域服務,然後使用雙重排程反轉呼叫域服務的方法。
什麼是雙重排程
exp:對於 “player” 實體,有乙個 “equip()” 方法來裝備 **。 一般情況下,“玩家”實體需要注入乙個 equipmentservice,但該實體只能保留自己的狀態,其他物件實體無法保證其完整性,所以我們不使用 equipmentservice;相反,它是通過方法引數引入的方式使用的。 即“玩家”實體"equip()"方法定義為:公共虛空裝備(武器、裝備服務裝備服務)}此方法稱為雙派模式。Double Dispatch 是使用域服務時經常使用的一種方法,類似於呼叫撤銷。
當單個行為直接修改多個實體時,它不能再由單個實體處理,而必須直接使用域服務的方法進行操作。 在這裡,域服務更像是乙個跨物件事務,確保跨多個實體的更改之間的一致性。
這種型別的域服務提供元件化行為,但不直接繫結到實體類本身。 這樣做的好處是,它可以通過元件化服務來減少**的重複。
介面被元件化以實現公共域服務。
exp:在遊戲系統中,原價、NPC、怪物都是移動的。 因此,可以設計乙個可移動的介面,讓玩家、NPC和怪物實體實現可移動的介面。 然後實現乙個 moveservice,從而實現乙個移動通用服務。
策略或策略設計模式是一種通用的設計模式,但經常出現在 DDD 架構中,其核心是封裝域規則。
策略是乙個無狀態的單一例項物件,通常至少需要 2 種方法:canapply 和 business 方法。
canapply 方法用於確定策略是否適用於當前上下文,如果是,則呼叫方觸發業務方法。 通常,為了降低策略的可測試性和複雜性,策略不應直接操作物件,而是通過返回計算值來對域服務中的物件進行操作。
什麼***“**也是一種域規則。 一般 *** 在另乙個物件上的核心域模型狀態更改後同步或非同步發生。 例如,當積分數達到 100 時,會員級別將公升級 1 級。
在 DDD 中,求解“**”的方法是域事件。 域事件可以通過 EventBus 事件匯流排進行傳播。
該領域時事的不足和前景由於實體需要保證完整性,因此不能直接依賴 EventBus,因此 EventBus 只能保留乙個全域性單例。 但是,全域性單例物件很難進行單次測試,這使得實體物件難以被完整的單次測試完全覆蓋。
通過對DDD的研究和實踐,我們越來越認識到,DDD作為一種軟體設計思路和指導,對大型複雜軟體的構建是非常有幫助的。 也為獅山歷史工程的重建提供了良好的指導方向。
作者:京東科技 孫黎明.*:京東雲開發者社群 **請註明**。