「首席架構看領域驅動設計」領域驅動的設計和開發最佳實踐
- 2019 年 11 月 27 日
- 筆記
背景
域驅動設計(DDD)是關於將業務域概念映射到軟件構件的。關於這個主題的大多數文章和文章都是基於Eric Evans的《領域驅動設計》一書,主要從概念和設計的角度覆蓋了領域建模和設計方面。這些文章討論了DDD的主要元素,如實體、價值對象、服務等,或者討論了泛在語言、有界上下文和反腐敗層等概念。
本文的目標是從一個實際的角度來討論如何獲取域模型並實際實現它,從而涵蓋域建模和設計。我們將查看技術主管和架構師在實現工作中可以使用的指導方針、最佳實踐、框架和工具。領域驅動的設計和開發還受到幾個體系結構、設計和實現方面的影響,比如:
- 業務規則
- 持久性
- 緩存
- 事務管理
- 安全
- 代碼生成
- 測試驅動開發
- 重構
本文討論了這些不同的因素是如何在項目的整個生命周期中影響項目的實現的,以及架構師在實現一個成功的DDD實現時應該注意什麼。我將從一個典型的域模型應該具有的特徵列表開始,以及何時在企業中使用域模型(與完全不使用域模型或使用貧血域模型相比)。
本文包括一個示例貸款處理應用程序,以演示如何在實際的域驅動開發項目中使用這裡討論的設計方面和開發最佳實踐。示例應用程序在實現貸款處理域模型時使用Spring、Dozer、Spring Security、JAXB、Arid pojo和Spring Dynamic模塊等框架。示例代碼將使用Java,但是對於大多數開發人員來說,無論其語言背景如何,都應該非常容易理解。
介紹
域模型提供了以下幾個好處:
- 它幫助團隊在公司的業務和It涉眾之間創建一個公共模型,團隊可以使用該模型來溝通業務需求、數據實體和流程模型。
- 模型是模塊化的,可擴展的,易於維護,因為設計反映了業務模型。
- 它提高了業務域對象的可重用性和可測試性。
另一方面,讓我們看看當IT團隊不遵循用於開發大中型企業軟件應用程序的域模型方法時會發生什麼。
不投資域模型和開發工作將導致應用程序體系結構「臃腫的服務層」和「貧血的域模型」,其中facade類(通常是無狀態會話bean)開始積累越來越多的業務邏輯,而域對象則變成只有getter和setter的數據載體。這種方法還會導致領域特定的業務邏輯和規則分散(在某些情況下還會重複)到幾個不同的facade類中。
貧血的領域模型,在大多數情況下,是不划算的;它們不會給公司帶來比其他公司更大的競爭優勢,因為在此體系結構中實現業務需求更改需要很長時間才能開發和部署到生產環境中。
在查看DDD實現項目中的不同體系結構和設計注意事項之前,讓我們先看看富域模型的特徵。
- 域模型應該關注特定的業務操作域。它應該與業務模型、策略和業務流程保持一致。
- 它應該與業務中的其他域以及應用程序體系結構中的其他層隔離。
- 它應該是可重用的,以避免相同核心業務域元素的任何重複模型和實現。
- 模型應該與應用程序中的其他層鬆散耦合設計,這意味着不依賴於域層(即數據庫層和facade層)任何一側的層。
- 它應該是一個抽象的、乾淨的獨立層,支持更容易的維護、測試和版本控制。域類應該在容器外部(和IDE內部)是單元可測試的。
- 它應該使用POJO編程模型進行設計,而不需要任何技術或框架依賴(我總是告訴我公司的項目團隊,我們用於軟件開發的技術是Java)。
- 域模型應該獨立於持久性實現細節(儘管技術確實對模型施加了一些約束)。
- 它應該對任何基礎架構框架具有最小的依賴性,因為它將比這些框架存在得更久,而且我們不希望任何外部框架上有任何緊密耦合。
為了在軟件開發工作上獲得更好的投資回報(ROI),業務部門和IT部門的高級管理人員必須致力於業務領域建模及其實現的投資(時間、金錢和資源)。讓我們看看實現域模型所需的其他一些因素。
- 團隊應該定期訪問業務領域的主題專家。
- IT團隊(建模人員、架構師和開發人員)應該具有良好的建模和設計技能。
- 分析師應該具有良好的業務流程建模技能。
- 架構師和開發人員應該具有很強的面向對象設計(OOD)和編程(OOP)經驗。
領域驅動設計在企業架構中的角色
領域建模和DDD在企業架構(EA)中扮演着重要的角色。自從EA的目標之一是保持IT與業務的單位,業務實體的域模型的表示,變成一個EA的核心部分。這就是為什麼大多數的EA組件(業務或基礎設施)應該在域模型設計和實現。
領域驅動設計和SOA
面向服務的體系結構(Service Oriented Architecture, SOA)在最近獲得了越來越多的動力,以幫助團隊構建基於業務流程的軟件組件和服務,並加快新產品的上市時間。域驅動設計是SOA體系結構的關鍵元素,因為它有助於將業務邏輯和規則封裝到域對象中。域模型還提供了用於定義服務契約的語言和上下文。
如果還沒有域模型,SOA工作應該包括域模型的設計和實現。如果我們過於強調SOA服務而忽略了域模型的重要性,那麼我們最終將得到一個貧血的域模型和應用程序體系結構中膨脹的服務。
一個理想的場景是,DDD工作通過迭代實現,同時開發應用層和SOA組件,因為它們是域模型元素的直接消費者。有了豐富的域實現,通過向域對象提供shell(代理),SOA設計將變得相對簡單。但是,如果我們過於關注SOA層,而在後端沒有像樣的域模型,則業務服務將調用不完整的域模型,這可能導致脆弱的SOA體系結構。
項目管理
域建模項目通常包括以下步驟:
- 首先對業務流程建模並編製文檔。
- 選擇一個候選業務流程,並與業務領域專家合作,使用通用語言對其進行文檔化。
- 標識候選業務流程所需的所有服務。這些服務可以是原子的(單個步驟),也可以是協調的(多步驟,有或沒有工作流)。它們也可以是業務(例如承銷或融資)或基礎設施(例如電子郵件或工作安排)。
- 標識並記錄上一步中標識的服務使用的對象的狀態和行為。
重要的是保持模型在高層次上,首先關注業務領域的核心元素。
從項目管理的角度來看,一個實際的DDD實施項目與任何其他軟件開發項目包含相同的階段。這些階段包括:
- 模型的域
- 設計
- 發展
- 單元和集成測試
- 基於設計和開發(模型概念的持續集成(CI)),細化和重構域模型。
- 使用更新的域模型(域實現的CI)重複上述步驟。
敏捷軟件開發方法非常適合這裡,因為敏捷方法關注業務價值的交付,就像DDD關注軟件系統與業務模型的一致性一樣。而且,由於DDD的迭代性質,SCRUM或DSDM等敏捷方法是管理項目的更好框架。使用SCRUM(用於項目管理)和XP(用於軟件開發)方法是管理DDD實現項目的良好組合。
DDD迭代周期的這個項目管理模型如下面的圖1所示。

圖1所示。DDD迭代周期圖(單擊屏幕快照打開全尺寸視圖)。
域驅動設計工作從域建模結束的地方開始。Ramnivas Laddad介紹了如何實現域對象模型的以下步驟。他強調在域模型中更多地關注域對象而不是服務。
- 從域實體和域邏輯開始。
- 開始時不使用服務層,只添加邏輯不屬於任何域實體或值對象的服務。
- 使用無所不在的語言、契約式設計(DbC)、自動化測試、CI和重構,使實現儘可能與域模型緊密一致。
從設計和實現的角度來看,一個典型的DDD框架應該支持以下特性。
- 它應該是一個基於POJO的框架(如果您的公司是一個. net商店,則應該是POCO)。
- 它應該支持使用DDD概念的業務領域模型的設計和實現。
- 它應該支持象依賴注入(DI)和面向方面編程(AOP)這樣的開箱即用的概念。(注意:本文後面將更詳細地解釋這些概念)。
- 集成單元測試框架,如JUnit, TestNG, Unitils等。
- 與其他Java/Java EE框架如JPA、Hibernate、TopLink等的良好集成。
樣例應用程序
本文使用的示例應用程序是一個住房貸款處理系統,業務用例是批准住房貸款(抵押貸款)的資金請求。當貸款申請被提交給抵押貸款公司時,首先要經過承銷商根據客戶的收入明細、信用記錄和其他因素批准或拒絕貸款申請的承銷過程。如果貸款申請被核保集團批准,則在貸款批准過程中要經歷關閉和融資的步驟。
貸款處理系統中的資金模塊自動處理向借款人發放資金的過程。融資過程通常從抵押貸款方(通常是銀行)將貸款包轉發給產權公司開始。然後,產權公司審查貸款包,並安排一個日期與賣方和買方的財產結束貸款。借款人和賣方與產權公司的交割代理人會面,簽署轉讓產權的文件。
體系結構
一個典型的企業應用架構由以下四個概念層組成:
- 用戶界面(表示層):負責向用戶表示信息和解釋用戶命令。
- 應用層:這一層負責協調應用程序活動。它不包含任何業務邏輯。它不保存業務對象的狀態,但可以保存應用程序任務進程的狀態。
- 域層:此層包含有關業務域的信息。業務對象的狀態保存在這裡。業務對象的持久性及其狀態可能被委託給基礎結構層。
- 基礎結構層:這一層作為所有其他層的支持庫。它提供層之間的通信,實現業務對象的持久性,包含用戶界面層的支持庫,等等。
讓我們更詳細地研究一下應用程序和域層。
應用程序層:
- 負責在應用程序中的UI屏幕之間導航,以及與其他系統的應用程序層的交互。
- 還可以對用戶輸入數據執行基本的(與業務無關的)驗證,然後再將其傳輸到應用程序的其他(較低的)層。
- 不包含任何業務或域相關邏輯或數據訪問邏輯。
- 沒有任何反映業務用例的狀態,但它可以管理用戶會話的狀態或任務的進度。
領域層:
- 負責業務領域的概念、關於業務用例和業務規則的信息。域對象封裝了業務實體的狀態和行為。處理貸款申請的業務實體包括抵押貸款、財產和借款人。
- 還可以管理業務用例的狀態(會話)如果用例跨多個用戶請求(如貸款登記流程,由多個步驟組成:用戶進入貸款細節,系統返回產品和基於貸款利率參數,用戶選擇一個特定的產品/率組合,最後系統鎖定的貸款利率)。
- 包含僅具有定義的不屬於任何域對象的操作行為的服務對象。服務封裝了不適合域對象本身的業務域行為。
- 是業務應用程序的核心,應該與應用程序的其他層隔離。而且,它不應該依賴於其他層(JSP/JSF、Struts、EJB、Hibernate、XMLBeans等)中使用的應用程序框架。
下面的圖2顯示了應用程序中使用的不同架構層以及它們與DDD的關係。

圖2。分層應用程序架構圖(單擊屏幕快照以打開全尺寸視圖)。
以下設計方面被認為是當前DDD實現配方的主要成分:
- 面向對象編程(OOP)
- 依賴注入(DI)
- 面向方面編程(AOP)
OOP是域實現中最重要的元素。應該利用繼承、封裝和多態性等OOP概念,使用普通的Java類和接口設計域對象。大多數域元素都是同時具有狀態(屬性)和行為(作用於狀態的方法或操作)的真對象。它們也符合現實世界的概念,並且能夠很好地適應面向對象編程的概念。DDD中的實體和值對象是OOP概念的經典示例,因為它們同時具有狀態和行為。
在一個典型的工作單元(UOW)中,域對象需要與其他對象協作,無論它們是服務、存儲庫還是工廠。域對象還需要管理其他關注點,如域狀態更改跟蹤、審計、緩存、事務管理(包括事務重試),這些實際上是橫切的。這些是可重用的與域無關的關注點,通常會分散在整個代碼(包括域層)中。將此邏輯嵌入到域對象中會導致域層與非域相關代碼的糾纏和混亂。
在沒有對象之間的緊密耦合和隔離橫切關注點的情況下管理代碼依賴項時,OOP本身無法為域驅動的設計和開發提供優雅的設計解決方案。在這裡,像DI和AOP這樣的設計概念可以用來補充OOP,從而最小化緊密耦合,增強模塊化,更好地管理橫切關注點。
依賴注入
DI是將配置和依賴項代碼移出域對象的好方法。另外,域類對數據訪問對象(DAO)類和服務類對域類的設計依賴性使得DI在DDD實現中成為「必須有的」。DI通過將其他對象(如存儲庫和服務)注入域對象,促進了更乾淨的鬆散耦合設計。
在樣例應用程序中,服務對象(FundingServiceImpl)使用DI注入實體對象(貸款、借款人和FundingRequest)。另外,實體通過DI引用存儲庫。類似地,其他Java EE資源(如數據源、Hibernate會話工廠和事務管理器)也被注入到服務和存儲庫對象中。
面向方面的編程
AOP通過從域對象中刪除審計、域狀態變化跟蹤等橫切關注點代碼來幫助更好的設計(即在域模型中減少混亂)。它可用於將協作對象和服務注入域對象,特別是未被容器實例化的對象(例如持久性對象)。域層中可以使用AOP的其他方面包括緩存、事務管理和基於角色的安全性(授權)。
貸款處理應用程序使用自定義方面將數據緩存引入服務對象。貸款產品和利率信息從數據庫表中加載一次(客戶端首先請求此信息),然後存儲在對象緩存(JBossCache)中,用於後續產品和利率查找。Product和rate數據經常被訪問,但是不經常更新,所以它是緩存數據而不是每次都命中後端數據庫的好選擇。
DI和AOP概念在DDD中的作用是最近一個討論線程中的主要主題。這個討論是基於Ramnivas Laddad的一個演講,他在演講中斷言,沒有AOP和DI的幫助,DDD是無法實現的。在演講中,Ramnivas談到了使用AOP使域對象重新獲得智能行為的「細粒度DI」概念。他提到域對象需要訪問其他細粒度對象來提供豐富的行為,對此的解決方案是將服務、工廠或存儲庫注入域對象(通過使用方面在構造函數或setter調用時注入依賴項)。
Chris Richardson還討論了使用DI、對象和方面來通過減少耦合和增加模塊化來改進應用程序設計。Chris談到了「大型服務」反模式,它是應用程序代碼耦合、糾纏和分散的結果,以及如何使用DI和AOP概念來避免它。
注釋
定義和管理方面和DI的一個最新趨勢是使用注釋。注釋有助於最小化實現遠程服務(如EJB或Web服務)所需的構件。它們還簡化了配置管理任務。Spring 2.5、Hibernate 3和其他框架充分利用了注釋來在Java企業應用程序的不同層中配置組件。
我們應該利用注釋來生成鍋爐板代碼,從而增加靈活性方面的價值。同時,應該謹慎使用注釋。它們應該用於在理解實際代碼時不會造成混淆或誤導的地方。使用注釋的一個很好的例子是Hibernate ORM映射,它增加了在類或屬性名旁邊指定SQL表名或列名的值。另一方面,像JDBC驅動程序配置(驅動程序名、JDBC url、用戶名和密碼)這樣的細節更適合存儲在XML文件中,而不是使用注釋。這是基於數據庫在相同上下文中的假設。如果需要在域模型和數據庫表之間進行重要的轉換,那麼設計應該考慮這個問題。
Java EE 5提供了諸如@Entity、@PersistenceUnit、@PersistenceContext等JPA注釋來為普通Java類添加持久性細節。在域建模的上下文中,實體、存儲庫和服務是使用注釋的很好選擇。
@ configured是Spring將存儲庫和服務注入域對象的方式。Spring框架將「域對象DI」的概念擴展到了@ configurationannotation之外。Ramnivas最近在博客中提到了即將發佈的Spring 2.5.2版本的最新改進(從項目快照構建379開始可用)。有三個新的方面(AnnotationBeanConfigurerAspect、AbstractInterfaceDrivenDependencyInjectionAspect和AbstractDependencyInjectionAspect)為域對象DI提供了簡單而靈活的選項。Ramnivas說,引入中間方面(AbstractInterfaceDrivenDependencyInjectionAspect)的主要原因是允許領域特定的注釋和接口發揮作用。Spring還提供了@Repository、@Service和@Transactional等其他注釋來幫助設計域類。
在示例應用程序中使用的一些注釋,實體對象(貸款、借款人和FundingRequest)使用@Entity注釋。這些對象還使用@ configurationannotation連接存儲庫對象。服務類使用@Transactional注釋用事務行為裝飾服務方法。
域模型和安全性
域層中的應用程序安全性確保只有經過授權的客戶機(人類用戶或其他應用程序)調用域操作並訪問域狀態。
Spring Security (Spring Portfolio中的子項目)在應用程序的表示層(基於URL)和域層(方法層)中提供了細粒度的訪問控制。框架使用Spring的Bean代理攔截方法調用並應用安全約束。它使用MethodSecurityInterceptor類為Java對象提供了基於角色的聲明性安全性。對於域對象,還存在以訪問控制列表(ACL的)形式的實例級安全性,以便在實例級控制用戶訪問。
使用Spring Security來管理域模型中的授權需求的主要優點是,該框架具有非侵入性的體系結構,因此我們可以在域和安全方面進行清晰的分離。而且,業務對象不會與安全實現細節混淆。我們可以在一個地方編寫通用的安全規則,並在需要實現它們的地方應用它們(使用AOP技術)。
在域和服務類中,授權是在類方法調用級別進行管理的。例如,「貸款批准」方法在承銷域對象可以調用任何用戶提供一個「保險人」的角色對貸款高達100萬美元而審批方法在相同的域對象的貸款申請,貸款金額大於100萬美元只能被一個用戶「承銷主管」角色。
下表總結了應用程序體系結構每一層中的各種應用程序安全問題。
表1 各種應用程序層中的安全問題

業務規則
業務規則是業務領域的重要組成部分。它們定義了需要應用於特定業務流程場景中的域對象的數據驗證和其他約束。業務規則通常分為以下幾類:
- 數據驗證
- 數據轉換
- 業務決策
- 流程路由(工作流邏輯)
語境在DDD世界中非常重要。上下文的特異性決定了域對象的協作以及其他運行時因素,如應用什麼業務規則等。驗證和其他業務規則總是在特定的業務上下文中處理。這意味着相同的域對象在不同的業務上下文中必須處理不同的業務規則集。例如,貸款域對象的某些屬性(如貸款金額和利率)在貸款通過貸款審批流程中的審批步驟後不能更改。但是,在為特定利率註冊和鎖定貸款時,可以更改相同的屬性。
儘管所有特定於域的業務規則都應該封裝在域層中,但是一些應用程序設計將這些規則放在facade類中,這導致域類在業務規則邏輯方面變得「貧血」。在小型應用程序中,這可能是一個可接受的解決方案,但是對於包含複雜業務規則的中型到大型企業應用程序,不推薦使用這種解決方案。更好的設計選項是將規則放在它們所屬的地方,即域對象中。如果業務規則邏輯跨越兩個或多個實體對象,那麼它應該成為服務類的一部分。
此外,如果我們不認真對待應用程序,設計業務規則最終將以代碼中幾個switch語句的形式編碼。隨着時間的推移,規則變得越來越複雜,開發人員不需要花時間重構代碼來將「switch」語句轉移到更易於管理的設計中。在類中硬編碼複雜的路由或決策規則邏輯會導致類中的方法變長、代碼重複,最終導致僵化的應用程序設計,從長遠來看,這將成為維護的噩夢。一個好的設計是將所有的規則(特別是隨着業務策略的變化而頻繁變化的複雜規則)放到一個規則引擎中(使用JBoss規則、OpenRules或Mandarax之類的規則框架),並從域類中調用它們。
驗證規則通常用不同的語言實現,如Javascript、XML、Java代碼和其他腳本語言。但是由於業務規則的動態性,腳本語言(如Ruby、Groovy或領域特定語言(DSL))是定義和管理這些規則的更好選擇。Struts(應用層)、Spring(服務)和Hibernate (ORM)都有自己的驗證模塊,我們可以在這些模塊中對傳入或傳出的數據對象應用驗證規則。在某些情況下,驗證規則也可以作為方面來管理(鏈接AOP規則的文章),這些方面可以被編織到應用程序的不同層(例如服務和控制器)中。
在編寫域類來管理業務規則時,一定要記住單元測試方面。規則邏輯中的任何更改都應該很容易在隔離狀態下進行單元測試。
示例應用程序包含一個業務規則集,用於驗證貸款參數是否在允許的產品和利率規範中。這些規則在腳本語言(Groovy)中定義,並應用於傳遞給FundingService對象的貸款數據。
設計
從設計的角度來看,域層應該有一個定義良好的邊界,以避免非核心域層的破壞,比如特定於供應商的轉換、數據過濾、轉換等。應該設計域元素來正確地保存域狀態和行為。基於狀態和行為,不同的域元素有不同的結構。下面的表2顯示了域元素及其包含的內容。
表2. 具有狀態和行為的域元素

包含狀態(數據)和行為(操作)的實體、值對象和聚合應該有明確定義的狀態和行為。同時,這種行為不應該超出對象邊界的限制。在用例中,實體應該根據它們的本地狀態完成大部分工作。但是他們不應該知道太多不相關的概念。
好的設計實踐是只包含用於封裝域對象狀態的屬性的getter /setter。在設計域對象時,僅為那些可以更改的字段提供setter方法。另外,公共構造函數應該只包含必需的字段,而不是包含域類中所有字段的構造函數。
在大多數用例中,我們實際上不必能夠直接更改對象的狀態。因此,與其更改內部狀態,不如使用更改後的狀態創建一個新對象並返回新對象。在這些用例中,這就足夠了,而且還減少了設計的複雜性。
聚合類向調用者隱藏協作類的用法。它們可用於在域類中封裝複雜的、介入的和依賴於狀態的需求。
支持DDD的設計模式
有幾種設計模式可以幫助領域驅動的設計和開發。以下是這些設計模式的列表:
- 域對象(做)
- 數據傳輸對象(DTO)
- DTO彙編
- 存儲庫:存儲庫包含以域為中心的方法,並使用DAO與數據庫交互。
- 泛型DAO的
- 時態模式:這些模式向豐富的域模型添加了時間維度。雙時態框架基於Martin Fowler的時態模式,為處理域模型中的雙時態問題提供了一種設計方法。可以使用諸如Hibernate之類的ORM產品來持久化核心域對象及其雙時態屬性。
DDD中使用的其他設計模式包括策略、外觀和工廠。Jimmy Nilsson在他的書中將工廠作為一個域模式進行了討論。
DDD反模式
在最佳實踐和設計模式的反面,有一些DDD的味道是架構師和開發人員在實現域模型時應該注意的。由於這些反模式,域層成為應用程序體系結構中最不重要的部分,而facade類在模型中扮演更重要的角色。以下是一些反模式:
- 貧血的域對象
- 重複DAO的
- 胖服務層:這是服務類最終擁有所有業務邏輯的地方。
- 特性嫉妒:這是Martin Fowler關於重構的書中提到的一種典型的味道,其中類中的方法對屬於其他類的數據太感興趣了。
數據訪問對象
DAO和存儲庫在域驅動設計中也很重要。DAO是關係數據庫和應用程序之間的契約。它封裝了來自web應用程序的數據庫CRUD操作的細節。另一方面,存儲庫是一個單獨的抽象,它與dao交互,並向域模型提供「業務接口」。
存儲庫使用域的通用語言,使用所有必要的dao,並以域所理解的語言為域模型提供數據訪問服務。
DAO方法是細粒度的,更接近於數據庫,而存儲庫方法是粗粒度的,更接近於域。另外,一個存儲庫類可能注入了多個DAO。存儲庫和DAO使域模型與處理數據訪問和持久性細節分離。
域對象應該僅依賴於存儲庫接口。這就是為什麼注入存儲庫而不是DAO會產生一個更乾淨的域模型的原因。不應該直接從客戶機(服務和其他使用者類)調用DAO類。客戶機應該總是調用域對象,而域對象又應該調用DAO來將數據持久化到數據存儲中。
管理域對象之間的依賴關係(例如,實體及其存儲庫之間的依賴關係)是開發人員經常遇到的一個經典問題。此問題的通常設計解決方案是讓服務或Facade類直接調用存儲庫,當調用存儲庫時,存儲庫將向客戶端返回實體對象。這種設計最終導致了前面提到的貧血域模型,其中facade類開始積累更多的業務邏輯,域對象成為純粹的數據載體。一個好的設計是使用DI和AOP技術將存儲庫和服務注入域對象。
樣例應用程序在實現貸款處理域模型時遵循這些設計原則。
持久性
持久性是一個基礎結構方面,應該對域層進行解耦。JPA通過對類隱藏持久性實現的細節來提供這種抽象。它是注釋驅動的,因此不需要XML映射文件。但同時,表名和列名被嵌入到代碼中,這在某些情況下可能不是一個靈活的解決方案。
使用提供數據網格解決方案的網格計算產品(如Oracle Coherence、WebSphere對象網格和GigaSpaces),開發人員在建模和設計業務域時甚至不需要考慮RDBMS。數據庫層以內存對象/數據網格的形式從域層抽象出來。
緩存
當我們討論域層的狀態(數據)時,我們必須討論緩存的方面。頻繁訪問的域數據(如按揭貸款處理應用程序中的產品和利率)是很好的緩存候選者。緩存可以提高性能並減少數據庫服務器上的負載。服務層是緩存域狀態的理想選擇。像TopLink和Hibernate這樣的ORM框架也提供了數據緩存。
貸款處理示例應用程序使用JBossCache框架來緩存產品和費率細節,以最小化數據庫調用並提高應用程序性能。
事務管理
事務管理對於保持數據完整性和提交或回滾UOW非常重要。關於在應用程序體系結構層中應該在何處管理事務,一直存在爭議。還有跨實體事務(跨越同一UOW中的多個域對象),它們影響應該在何處管理事務的設計決策。
有些開發人員喜歡在DAO類中管理事務,這是一個糟糕的設計。這導致了過於細粒度的事務控制,這沒有提供管理事務跨多個域對象的用例的靈活性。服務類應該處理事務;這樣,即使事務跨越多個域對象,服務類也可以管理事務,因為在大多數用例中,服務類處理控制流。
示例應用程序中的FundingServiceImpl類管理資金請求的事務,並通過調用存儲庫執行多個數據庫操作,並在單個事務中提交或回滾所有數據庫更改。
數據傳輸對象
DTO也是SOA環境中設計的一個重要部分,在SOA環境中,域對象模型在結構上與從業務服務接收和發送的消息不兼容。消息通常在XML模式定義文檔(XSD)中定義和維護,從XSD中編寫(或代碼生成)DTO對象並將其用於域和SOA服務層之間的數據(消息)傳輸是一種常見的實踐。在分佈式應用程序中,將數據從一個或多個域對象映射到一個DTO將成為一個必要的麻煩,因為從性能和安全角度來看,通過網絡發送域對象可能並不實際。
從DDD的角度來看,DTO還有助於維護服務層和UI層之間的分離,其中DO用於域,服務層用於表示層,DTO用於表示層。
Dozer框架用於將一個或多個域對象組裝到一個DTO對象中。它是雙向的,這節省了大量額外的代碼和時間轉換域對象到DTO的,反之亦然。DO和DTO對象之間的雙向映射有助於消除單獨的DO -> DTO和DTO -> DO轉換邏輯。框架還正確處理類型和數組轉換。
當請求進入資金處理時,樣例應用程序使用Dozer映射文件(XML)將FundingRequestDTO對象分割為貸款、借款人和FundingRequest實體對象。該映射還負責將來自實體的資金響應數據聚合到返回客戶端的單個DTO對象中。
DDD實施框架
Spring和Real Object Oriented (ROO)、Hibernate等框架有助於設計和實現域模型。其他支持DDD實現的框架有JMatter、Naked Objects、Ruby On Rails、Grails和Spring Modules XT框架。
Spring負責實例化和連接域類(如服務、工廠和存儲庫)。它還使用@ configurationannotation將服務注入實體。該注釋是特定於Spring的,因此實現此注入的其他選項是使用諸如Hibernate攔截器之類的東西。
ROO是一個建立在「領域第一,基礎設施第二」理念上的DDD實現框架。開發該框架是為了減少web應用程序開發中模式的樣板代碼。在使用ROO時,我們定義域模型,然後框架(基於Maven原型)為模型-視圖-控制器(MVC)、DTO、業務層Facade和DAO層生成代碼。它甚至為單元測試和集成測試生成存根。
ROO有一些非常實用的實現模式。例如,它區分狀態管理的字段,持久層使用字段級訪問,公共構造函數只反映強制字段。
開發
沒有實際的實現,模型是沒有用的。實現階段應該包括儘可能多地自動化開發任務。要查看哪些任務可以自動化,讓我們來看一個涉及域模型的典型用例。以下是用例中的步驟列表:
請求:
- 客戶端調用Facade類,以XML文檔的形式發送數據(與XSD兼容);Facade類為UOW啟動一個新的事務。
- 對輸入的數據運行驗證。這些驗證包括主要的(基本的/數據類型/字段級別的檢查)和業務驗證。如果存在任何驗證錯誤,則提出適當的異常。
- 將描述翻譯成代碼(對域友好)。
- 使數據格式更改對域模型友好。
- 對屬性進行任何分離(例如將客戶名拆分為customer實體對象中的first和last name屬性)。
- 將DTO數據分解為一個或多個域對象。
- 持久化域對象的狀態。
響應:
- 從數據存儲中獲取域對象的狀態。
- 必要時緩存狀態。
- 將域對象組裝到應用程序友好的數據對象(DTO)中。
- 對數據元素進行任何合併或分離(例如將姓和名合併到單個客戶名屬性中)。
- 把代碼翻譯成描述。
- 對數據格式進行必要的更改,以滿足客戶端數據使用需求。
- 必要時緩存DTO狀態
- 當控制流退出時,事務提交(或回滾)。
下表顯示了在應用程序中將數據從一個層傳送到另一個層的不同對象。
表3. 數據流經應用程序層

正如您所看到的,在應用程序架構中有幾個層,其中相同的數據以不同的形式(DO、DTO、XML等)流動。這些包含數據和其他類(如DAO、DAOImpl和DAOTest)的大多數對象(Java或XML)本質上都是基礎結構。這些具有樣板代碼和結構的類和XML文件非常適合用於代碼生成。
代碼生成
ROO之類的框架還為新項目創建了一個標準的、一致的項目模板(使用Maven插件)。使用預先生成的項目模板,我們可以在目錄結構中實現一致性,在哪裡存儲源和測試類、配置文件,以及內部和外部(第三方)組件庫的依賴性。
當我們考慮到開發一個典型的企業軟件應用程序需要大量的類和配置文件時,這可能會讓人難以承受。代碼生成是解決這個問題的最佳方法。代碼生成工具通常使用某種模板框架來定義模板或映射,代碼生成器可以從這些模板或映射生成代碼。Eclipse Modeling Framework (EMF)有幾個子項目,可以幫助生成web應用程序項目中所需的各種構件的代碼。像AndroMDA這樣的模型驅動架構(Model Driven Architecture, MDA)工具使用EMF根據架構模型生成代碼。
當涉及到在域層中編寫委託類時,我看到開發人員手動編寫這些類(主要是從頭開始編寫第一個類,然後按照「複製和粘貼」模式為其他域對象創建所需的委託類。由於大多數這些類基本上都是域類的外觀,所以它們是代碼生成的良好候選對象。代碼生成選項是一個很好的長期解決方案,即使它涉及一些初始投資(在編碼和時間方面)來構建和測試代碼生成器(引擎)。
對於生成的測試類,一個好的選擇是為需要進行單元測試的主類中具有複雜業務邏輯的方法創建抽象方法。通過這種方式,開發人員可以擴展生成的基本測試類,並實現不能自動生成的自定義業務邏輯。對於任何具有不能自動創建的測試邏輯的測試方法都是一樣的。
腳本語言是編寫代碼生成器的更好選擇,因為它們的開銷更少,並且支持模板創建和自定義選項。如果我們利用DDD項目中的代碼生成,我們只需要從頭開始編寫幾個類。必須從頭創建的工件包括:
- XSD
- 域對象
- 服務
一旦我們定義了XSD和Java類,我們就可以通過代碼生成以下所有或大部分類和配置文件:
- DAO接口和實現類
- 工廠
- 存儲庫
- 域委託(如果需要)
- Facade(包括EJB和web服務類)
- DTO的
- 以上類的單元測試(包括測試類和測試數據)
- Spring配置文件
下面的表4列出了web應用程序體系結構中的不同層,以及可以在該層生成什麼工件(Java類或XML文件)。
表4:DDD實現項目中的代碼生成

委託層是唯一同時具有領域對象和DTO知識的層。其他層,如持久層,應該不知道DTO的。
重構
重構是在不改變應用程序的功能或行為的情況下改變或重組應用程序代碼。重構可以與設計或代碼相關。進行設計重構是為了不斷地細化模型並重構代碼以改進域模型。
重構在DDD項目中扮演着重要的角色,因為它具有領域建模的迭代和進化性質。將重構任務集成到項目中的一種方法是在調用迭代完成之前將其添加到項目的每個迭代中。理想情況下,重構應該在每個開發任務之前和之後進行。
重構應該嚴格遵守規則。結合使用重構、CI和單元測試來確保代碼更改不會破壞任何功能,同時這些更改確實有助於預期的代碼或性能改進。
自動化測試在重構應用程序代碼中起着至關重要的作用。如果沒有良好的自動化開發人員測試和測試驅動開發(TDD)實踐,重構可能會適得其反,因為沒有自動的方法來驗證作為重構工作一部分的設計和代碼更改不會改變行為或破壞功能。
Eclipse之類的工具可以幫助以迭代的方式實現域模型,並將重構作為開發工作的一部分。Eclipse具有諸如提取或將方法移動到不同的類或將方法下推到子類等特性。還有一些Eclipse的代碼分析插件可以幫助管理代碼依賴項和識別DDD反模式。當我對項目進行設計和代碼評審時,我依賴JDepend、Classycle和Metrics等插件來評估應用程序中域和其他模塊的質量。
Chris Richardson談到了使用Eclipse提供的重構特性,應用代碼重構將過程設計轉換為面向對象設計。
單元測試/持續集成
我們前面談到的目標之一是,域類應該是單元可測試的(在初始開發期間以及稍後重構現有代碼時),而不需要對容器或其他基礎結構代碼有太多依賴。TDD方法幫助團隊在項目的早期發現任何設計問題,並驗證代碼是否與域模型一致。DDD對於測試優先的開發是理想的,因為狀態和行為包含在域類中,並且應該很容易對它們進行隔離測試。重要的是測試域模型的狀態和行為,而不是過多地關注數據訪問或持久性的實現細節。
像JUnit或TestNG這樣的單元測試框架是實現和管理域模型的好工具。其他測試框架,如DBUnit和Unitils,也可以用來測試域層,特別是將測試數據注入到DAO類中。這將最小化為在單元測試類中填充測試數據而編寫的額外代碼。
模擬對象還有助於在隔離狀態下測試域對象。但是重要的是不要在域層中瘋狂地使用模擬對象。如果有其他測試域類的簡單方法,您應該使用這些選項,而不是使用模擬對象。例如,如果您可以使用後端中真實的DAO類(而不是模擬DAO實現)和內存中的HSQL數據庫(而不是真實數據庫)來測試實體類;它將使域層單元測試運行得更快,這是使用模擬對象背後的主要思想。這樣,您將測試域對象之間的協作(交互)以及它們之間交換的狀態(數據)。對於模擬對象,我們將只測試域對象之間的交互。
一旦開發任務完成,在開發階段創建的所有單元和集成測試(使用或不使用TDD實踐)都將成為自動化測試套件的一部分。應該在本地和更高的開發環境中頻繁地維護和執行這些測試,以確定新代碼更改是否將任何bug引入了域類。
Eric Evans在他的書中談到了CI,他說CI工作應該總是在有限的上下文中應用,它應該包括人和代碼的同步。CI工具比如CruiseControl和哈德遜可以用來建立一個自動構建和測試環境中運行應用程序構建腳本(使用Ant或Maven這樣的構建工具創建)檢出代碼從SCM存儲庫(如CVS, Subversion等),編譯域類(以及其他類的應用程序),如果沒有構建錯誤,然後自動運行所有的測試(單元測試和集成)。如果有任何構建或測試錯誤,也可以設置CI工具來通知項目團隊(通過電子郵件或RSS提要)。
部署
域模型從不是靜態的;它們隨着項目生命周期中業務需求的演進和新項目中出現的新需求而變化。另外,在開發和實現域模型時,您需要不斷地學習和改進,並希望將新知識應用到現有的模型中。
在打包和部署域類時,隔離是關鍵。由於域層的一端依賴於DAO層,另一端依賴於服務Facade層(參見圖2中的應用程序體系結構關係圖),因此將域類打包並部署為一個或多個模塊以優雅地管理這些依賴關係非常有意義。
雖然DI、AOP和工廠等設計模式在設計時最小化了對象之間的耦合併使應用程序模塊化,但OSGi(以前稱為開放服務網關計劃)在運行時解決了模塊化問題。OSGi正在成為打包和分發企業應用程序的標準機制。它很好地處理了模塊之間的依賴關係。我們還可以使用OSGi進行域模型版本控制。
我們可以將DAO類打包在一個OSGi包中(DAO包),將服務facade類打包在另一個包中(服務包),因此當修改DAO或服務實現或部署應用程序的不同版本時,由於OSGi,不需要重新啟動應用程序。如果為了向後兼容而必須支持某些域對象的現有版本和新版本,我們還可以部署同一個域類的兩個不同版本。
為了利用OSGi的功能,應用程序對象在被使用之前必須在OSGi平台上註冊(也就是說,在客戶端對它們進行查找之前)。這意味着我們必須使用OSGi api來進行註冊,但是我們還必須在服務啟動和停止使用OSGi容器時處理故障場景。Spring Dynamic Modules框架通過允許在應用程序中導出和導入任何類型的對象而不需要修改任何代碼,在這方面提供了幫助。
Spring DM還提供了在容器外運行OSGi集成測試的測試類。例如,AbstractOsgiTests可用於直接從IDE運行集成測試。設置由測試基礎結構處理,因此我們不必編寫清單。MF文件進行測試,或做任何打包或部署。該框架支持當前可用的大多數OSGi實現(Equinox、Knopflerfish和Apache Felix)。
貸款處理應用程序使用OSGi、Spring DM和Equinox容器來管理模塊級依賴項以及域和其他模塊的部署。LoanAppDeploymentTests展示了Spring DM測試模塊的使用。
示例應用程序設計
貸款處理樣本申請中使用的域類如下:
實體:
- Loan
- Borrower
- UnderwritingDecision
- FundingRequest
值對象:
- ProductRate
- State
服務:
- FundingService
存儲庫:
- LoanRepository
- BorrowerRepository
- FundingRepository
圖3顯示了示例應用程序的域模型圖。

圖3。分層應用程序域模型(單擊屏幕快照打開全尺寸視圖)。
本文中討論的大多數DDD設計概念和技術都應用於示例應用程序。使用了諸如DI、AOP、注釋、域級安全性和持久性等概念。此外,我還使用了幾個開源框架來幫助完成DDD開發和實現任務。這些框架如下:
- Spring
- Dozer
- Spring Security
- JAXB (Spring-WS for marshalling and unmarshalling the data)
- Spring Testing (for unit and integration testing)
- DBUnit
- Spring Dynamic Modules
樣例應用程序中的域類使用Equinox和Spring DM框架部署為一個OSGi模塊。下表顯示了示例應用程序的模塊打包細節。
表5所示。打包和部署細節

結論
DDD是一個強大的概念,它將改變建模人員、架構師、開發人員和測試人員在團隊接受了DDD培訓並開始應用「領域第一,基礎設施第二」的理念之後看待軟件的方式。不同利益相關者(從IT和業務單位)與不同背景和領域的專業知識參與域建模、設計和實現工作,引用Eric Evans,「重要的是不要模糊的哲學之間的線路設計(DDD)和技術工具框,幫助我們完成它(OOP, DI和AOP)」。
推進前沿
本節介紹一些影響DDD設計和開發的新方法。其中一些概念仍在發展中,看看它們將如何影響DDD將是很有趣的。
體系結構規則和契約實施設計在域模型標準和實現最佳實踐的治理和策略實施中扮演重要角色。Ramnivas談到了使用方面來執行只通過工廠創建存儲庫對象的規則;這是一個容易違反設計規則在領域層。
領域特定語言(DSL)和業務自然語言(BNL)近年來受到越來越多的關注。可以使用這些語言表示域類中的業務邏輯。BNL的強大之處在於,它們可以用來捕獲業務規範、記錄業務規則,以及作為可執行代碼。它們還可以用來創建測試用例,以驗證系統是否按預期工作。
行為驅動開發(BDD)是最近討論的另一個有趣的概念。BDD通過提供跨越業務和技術之間的鴻溝的公共詞彙表(普遍存在的語言),幫助將開發重點放在交付優先級高的、可驗證的業務價值上。通過使用關注於系統的行為方面而不是測試方面的術語,BDD試圖幫助開發人員將注意力集中在TDD最成功的地方的真正價值上。如果正確地實踐,BDD可以成為DDD的一個很好的補充,在DDD中,領域對象的開發受到BDD概念的積極影響;畢竟,所有的域對象都應該封裝狀態和行為。
事件驅動架構(EDA)是另一個可以在領域驅動設計中發揮作用的領域。例如,用於通知域對象實例中的任何狀態更改的事件模型將有助於處理需要在域對象的狀態更改時觸發的事件後處理任務。EDA有助於封裝基於事件的邏輯,從而避免嵌入到核心域邏輯中。Martin Fowler記錄了關於域事件設計模式的內容。
資源
- 領域驅動設計,解決軟件核心的複雜性,Eric Evans, Addison Wesley
- 應用領域驅動的設計和模式,Jimmy Nilsson, Addison Wesley
- 《重構到模式》,Joshua Kerievsky, Addison Wesley著
- DDD可以在沒有DI和AOP的情況下充分實現嗎?
原文:https://www.infoq.com/articles/ddd-in-practice/
本文:https://pub.intelligentx.net/node/823
討論:請加入知識星球【首席架構師圈】或者飛聊小組【首席架構師智庫】