領域驅動設計和開發實戰

背景

領域驅動設計(DDD)的中心內容是如何將業務領域概念映射到軟件工件中。大部分關於此主題的著作和文章都以Eric Evans的書《領域驅動設計》爲基礎,主要從概念和設計的角度探討領域建模和設計情況。這些著作討論實體、值對象、服務等DDD的主要內容,或者談論通用語言、界定的上下文(Bounded Context)和防護層(Anti-Corruption Layer)這些的概念。

本文旨在從實踐的角度探討領域建模和設計,涉及如何着手處理領域模型並實際地實現它。我們將着眼於技術主管和架構師在實現過程中能用到的指導方針、最佳實踐、框架及工具。領域驅動設計和開發也受一些架構、設計、實現方面的影響,比如:

  • 業務規則
  • 持久化
  • 緩存
  • 事務管理
  • 安全
  • 代碼生成
  • 測試驅動開發
  • 重構

本文討論這些不同的因素在項目實施的整個生命週期中怎樣對其產生影響,還有架構師在實現成功的DDD中應該去尋求什麼。我會先列出領域模型應該具備的典型特徵,以及何時在企業中使用領域模型(相對於根本不使用領域模型,或使用貧血的領域模型來說)。

文章包括一個貸款處理示例應用,來演示如何將設計立場、以及這裏討論的開發最佳實踐,應用在真實的領域驅動開發項目之中。示例應用用了一些框架去實現貸款 處理領域模型,比如Spring、Dozer、Spring Security、JAXB、Arid POJOs和Spring Dynamic Modules。示例代碼用Java編寫,但對大多數開發人員來說,不論語言背景如何,代碼都是很容易理解的。

引言

領域模型帶來了一些好處,其中有:

  • 有助於團隊創建一個業務部門與IT部門都能理解的通用模型,並用該模型來溝通業務需求、數據實體、過程模型。
  • 模型是模塊化、可擴展、易於維護的,同時設計還反映了業務模型。
  • 提高了業務領域對象的可重用性和可測性。

反過來,如果IT團隊在開發大中型企業軟件應用時不遵循領域模型方法,我們看看會發生些什麼。

不投放資源去建立和開發領域模型,會導致應用架構出現“肥服務層”和“貧血的領域模型”,在這樣的架構中,外觀類(通常是無狀態會話Bean)開始積聚越 來越多的業務邏輯,而領域對象則成爲只有getter和setter方法的數據載體。這種做法還會導致領域特定業務邏輯和規則散佈於多個的外觀類中(有些 情況下還會出現重複的邏輯)。

在大多數情況下,貧血的領域模型沒有成本效益;它們不會給公司帶來超越其它公司的競爭優勢,因爲在這種架構裏要實現業務需求變更,開發並部署到生產環境中去要花費太長的時間。

在考慮DDD實現的項目中各種架構和設計因素之前,讓我們先看看富領域模型的特性:

  • 領域模型應該側重於具體的業務操作領域。它應該結合業務模型、策略和業務流程。
  • 它應該與業務中的其它領域,還有應用架構中的其它層隔離開來。
  • 它應該可重用,以避免相同的核心業務領域元素有任何重複的模型和實現。
  • 模型應該設計得與應用中的其它層鬆耦合,這意味着領域層與上下兩層(即數據庫和外觀類)都沒有依賴關係。
  • 它應當是一個抽象的、清晰劃分的層次,以使維護、測試、版本處理更容易。可在容器外(從IDE中)對領域類進行單元測試。
  • 它應該用POJO編程模型來設計,沒有任何技術或框架依賴性(我總是告訴公司裏我工作的項目團隊,我們軟件開發用的技術是Java)。
  • 領域模型應該獨立於持久化實現的細節(儘管技術確實會對模型有一些限制)。
  • 它應該最小程度地依賴於任何基礎設施框架,因爲它將比這些框架更經久,我們也不希望與任何外部框架緊耦合。

爲了實現軟件開發中更高的投資回報率(ROI),業務單位和IT的高級管理人員必須在業務領域建模及其實現的投資上(時間、金錢和資源)全力以赴。讓我們來看看實現領域模型需要的其它因素。

  • 團隊應該經常接近業務領域主題專家。
  • IT團隊(建模者、架構師和開發人員)應具備良好的建模、設計技能。
  • 分析師應該具有良好的業務流程建模技能。
  • 架構師和開發人員應該有豐富的面向對象設計(OOD)和編程(OOP)經驗。

領域驅動設計在企業架構中的作用

領域建模和DDD在企業架構(EA)中發揮着重要的作用。因爲EA的目標之一就是結合IT和業務部門,業務實體的代表——領域模型就是EA的核心部分。這就是爲什麼大多數EA組件(業務或基礎設施)應該圍繞領域模型設計和實現的原因。

領域驅動設計和SOA

面向服務的體系架構(SOA)最近幫助團隊構建基於業務流程的軟件構件和服務、加速新產品上市時間的勢頭越來越強勁。領域驅動設計是SOA的一個關鍵因素,因爲它有助於封裝領域對象中的業務邏輯和規則。領域模型也提供了定義服務契約使用的語言和上下文。

如果還沒有領域模型,SOA的實行就應該包括領域模型的設計和實現。如果我們太過強調SOA服務、忽略了領域模型的重要性,那我們在應用架構中最終得到的就是一個貧血的領域模型和臃腫的服務。

理想的情況是,在開發應用層和SOA組件的同時,迭代地實現DDD,因爲應用層和SOA組件都是領域模型要素的直接消費者。使用豐富的領域實現,通過給領 域對象提供一個殼(代理),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)這些概念的開箱即用。(注:稍後將在文章中詳細解釋這些概念)。
  • 與單元測試框架整合,比如JUnitTestNGUnitils等。
  • 與其它Java/Java EE框架進行良好的集成,比如JPA、Hibernate、TopLink等。

示例應用

本文中使用的示例應用是一個住房貸款處理系統,業務用例是批准住房貸款(抵押)的資金申請。將貸款申請提交給抵押放貸公司的 時候,首先要通過承保過程,承保人在這一過程中根據客戶的收入詳情、信用歷史記錄和其它因素來決定批准還是拒絕貸款請求。如果貸款申請獲得承保組的批准, 就進入貸款審批程序的結清和融資步驟。

貸款處理系統中的融資模塊自動給貸款人支付資金。通常,融資過程從抵押放貸公司(通常是銀行)將貸款包遞交給產權公司開始。接着產權公司評估貸款包,並與房產買賣雙方一起確定結清貸款的時間。貸款人和賣方與結算中介在產權公司會面、簽署書面協議,來轉移房產產權。

架構

典型的企業應用架構由下面四個概念上的層組成:

  • 用戶界面(表現層):負責給用戶展示信息,並解釋用戶命令。
  • 應用層:該層協調應用程序的活動。不包括任何業務邏輯,不保存業務對象的狀態,但能保存應用程序任務過程的狀態。
  • 領域層:這一層包括業務領域的信息。業務對象的狀態在這裏保存。業務對象的持久化和它們的狀態可能會委託給基礎設施層。
  • 基礎設施層:對其它層來說,這一層是一個支持性的庫。它提供層之間的信息傳遞,實現業務對象的持久化,包含對用戶界面層的支持性庫等。

讓我們更詳細地看一下應用層和領域層。應用層:

  • 負責應用中UI屏幕之間的導航,以及與其它系統應用層之間的交互。
  • 還能對用戶輸入的數據進行基本(非業務相關)的驗證,然後再把數據傳到應用的其它層(更底層)。
  • 不包含任何業務、領域相關的邏輯、或數據訪問邏輯。
  • 沒有任何反映商業用例的狀態,但卻能處理用戶會話或任務進展的狀態。

領域層:

  • 負責業務領域的概念,業務用例和業務規則的相關信息。領域對象封裝了業務實體的狀態和行爲。貸款處理應用中的業務實體例子有抵押(Mortgage)、房產(Property)和貸款人(Borrower)。
  • 如果用例跨越多個用戶請求(比如貸款登記過程包含多個步驟:用戶輸入貸款詳細信息,系統基於貸款特性返回產品和利率,用戶選擇特定的產品/利率組合,最後系統會用這個利率鎖定貸款),還可以管理業務用例的狀態(會話)。
  • 包含服務對象,這些服務對象只包含一個定義好的、不屬於任何領域對象的可操作行爲。服務封裝了業務領域的狀態,而業務領域並不適用於領域對象本身。
  • 是商業應用的核心,應該與應用的其它層隔離開來。而且,它不應該依賴於其它層使用的應用框架(JSP/JSF、Struts、EJB、HibernateXMLBeans等)。

下面的圖2顯示了應用中使用的不同架構層次,以及它們與DDD有怎樣的關係。


圖2. 多層應用架構圖(點擊查看大圖)

下面的設計觀點被認爲是目前DDD實現訣竅的主要部分:

  • 面向對象編程(OOP
  • 依賴注入(DI
  • 面向方面編程(AOP

OOP是領域實現中最重要的基本原則。應該利用像繼承、封裝和多態這樣的OOP概念,使用Plain Java類和接口來設計領域對象。大部分領域元素是既有狀態(屬性)又有行爲(操作狀態的方法或操作)的真正對象。它們同時對應於真實世界的概念,能很合 適地適用於OOP概念。DDD中的實體和值對象都是OOP概念的典型例子,因爲它們同時有狀態和行爲。

在典型的工作單元(UOW)中,領域對象需要與其它的對象協作,無論這些對象是服務、資源庫、還是工廠。領域對象還需要處理其它那些本身就橫切的關注點, 比如領域狀態變化跟蹤、審計、緩存、事務管理(包括事務重試)。這些都是可重用、非領域相關的關注點,通常很容易在包括領域層的整個代碼中散佈和重複。在 領域對象中嵌入該邏輯會導致領域層和非領域相關的代碼互相糾纏、產生混亂。

說到處理對象間之沒有緊耦合的代碼依賴關係和隔離橫切關注點的時候,OOP並不能獨自爲領域驅動設計和開發提供極好的設計解決方案。在這是可以利用DI和AOP這樣的設計概念對OOP進行補充,以儘量減少緊耦合、提高模塊化、更好地處理橫切關注點。

依賴注入

DI能很有效地將配置和依賴代碼從領域對象中移出。此外,領域類對數據訪問對象(DAO)類、服務類對領域類的設計依賴性使得DI成爲DDD實現中“必須有”的內容。通過將資源庫和服務之類的其它對象注入到領域對象,DI有助於創建一個更清晰、鬆耦合的設計。

在示例應用中,服務對象(FundingServiceImpl)利用DI注入實體對象(Loan、Borrower和FundingRequest)。實體也通過DI引用資源庫。同樣的,像數據源、Hibernate會話工廠事務管理器這些其它的Java EE資源也被注入到服務和資源庫對象中。

面向方面編程

通過從領域對象中移除橫切關注點代碼,比如檢查、領域狀態變化跟蹤等,AOP有助於實現一個更好的設計(即在領域模型中少一些亂七八糟的內容)。可利用 AOP把協同對象和服務注入領域對象,特別是那些容器沒有實例化的對象(比如持久化對象)。在可以利用AOP的領域層中,其它的方面有緩存、事務管理和基 於角色的安全(授權)。

貸款處理應用利用自定義方面將數據緩存引入服務對象。貸款產品和利率信息從數據庫表中加載一次(客戶端第一次請求這些信息時),然後存儲到適用於後面產品和利率查找的對象緩存(JBossCache)中。產品和利率會被頻繁訪問,但不會定期更新,所以緩存數據是一個很好的候選方案,而不是每次都從後端的數據庫獲取。

在近期的討論貼子裏,DDD中DI和AOP概念的作用是主要的話題。討論以Ramnivas Laddad的演講爲基礎,Ramnivas在其演講中主張,沒有AOP和DI的幫助,DDD無法實現。 Ramnivas在這個演講中討論了“細粒度DI”的概念,這一概念利用AOP使領域對象恢復機敏性。他說領域對象需要訪問其它細粒度的對象來提供豐富的 行爲,該問題的解決方案是在領域對象中注入服務、工廠或資源庫(通過在調用構造或setter方法時期使用方面來注入依賴)。

Chris Richardson也討論了有關利用DI、對象和方面,通過減少耦合、提高模塊化來改進應用設計。Chris談到了“超級大服務”反模式,這是應用代碼耦合、混亂、分散的結果,他還談了如何利用DI和AOP的概念來避免這一反模式。

註解

最近定義、處理方面和DI的趨勢是使用註解。對實現遠程服務(比如EJB或Web Services)來說,註解有助於減少所需的工件。它們還簡化了配置管理任務。Spring 2.5Hibernate 3,以及其它框架都充分利用註解在Java企業應用的不同層中配置組件。

我們應該利用註解生成模板代碼,模板代碼能在靈活性上增加價值。但同時應該謹慎使用註解。註解應該用於不會引起混淆或誤解實際代碼的地方。使用註解的一個 很好的例子是Hibernate ORM映射,註解能直接用類或屬性名給指定的SQL表或列名添加值。另一方面,像JDBC驅動配置(驅動類名、JDBC URL、用戶名和密碼)這樣的詳細信息則更適合於存放在XML文件中,而不是使用註解。這基於數據庫在同一個上下文中這一假設。如果領域模型和數據庫表之 間需要相當多的轉換,那就應該好好思考一下設計了。

Java EE 5提供JPA註解,比如@Entity@PersistenceUnit@PersistenceContext等,以此給簡單的Java類添加持久化細節。在領域建模上下文中,實體、資源庫和服務都是使用註解的好地方。

@Configurable是 Spring將資源庫和服務注入領域對象的方式。Spring框架在@Configurable註解之上擴展了“領域對象依賴注入”思想。 Ramnivas最近在博客中談論了即將發佈的Spring 2.5.2版本(從項目的Snapshot Build 379開始可用)的最新改進。 有三個新的方面(AnnotationBeanConfigurerAspect、 AbstractInterfaceDrivenDependencyInjectionAspect和 AbstractDependencyInjectionAspect)爲領域對象依賴注入提供了簡單、更靈活的選擇。Ramnivas說,引入中間的方 面(AbstractInterfaceDrivenDependencyInjectionAspect),其主要原因是要讓領域特定的註解和接口發揮 作用。Spring還提供了其它註解來幫助設計領域對象,比如@Repository@Service@Transactional

示例應用中使用了部分註解。實體對象(Loan、Borrower和FundingRequest)使用了@Entity註解;這些對象還使用@Configurable註解綁定資源庫對象;服務類也使用@Transactional註解來用事務行爲裝飾服務方法。

領域模型和安全

領域層的應用安全確保只有授權的客戶端(人類用戶或其它應用)能調用領域操作,訪問領域狀態。

Spring安全(Spring Portfolio的一個子項目)同時爲應用的表現層(以URL爲基礎)和領域層(方法級)提供了細粒度的訪問控制。該框架使用Spring的Bean Proxy來攔截方法調用,運用安全約束。它爲使用MethodSecurityInterceptor類的Java對象提供了基於角色的聲明式安全。它也有針對領域對象的訪問控制列表(ACL's)形式的實例級別安全,以控制實例級別的用戶訪問。

在領域模型中使用Spring安全來處理授權需求的主要好處是,框架有一個非侵入式的架構,我們可以完全隔離領域和安全方面。此外,業務對象也不會和安全實現細節混成一團。我們可以只在一個地方編寫通用的安全規則,(使用AOP技術)在任何需要實現它們的地方運用它們。

在領域和服務類中,授權在類方法調用級別進行處理。舉例來說,對於高達一百萬美元的貸款,承保領域對象中的“貸款審批”方法可以由任何具有“承保人”角色 的用戶調用;而對於超過一百萬美元的貸款申請來說,同一領域對象中的審批方法則只能由具有“覈保主管”角色的用戶調用。

下表簡要說明了應用架構每一層中應用的各種安全關注點。

表1. 各個應用層中的安全關注點

安全關注點
客戶端/控制器 認證、Web頁面(URL)界別授權
外觀 基於角色的授權
領域 領域實例級別授權、ACL
數據庫 DB對象級別授權(存儲過程、存儲函數、觸發器)

業務規則

業務規則是業務領域中的重要部分。它們定義了數據驗證和其它的約束規則,這些規則需要應用於特定業務流程場景中的領域對象。業務規則通常分爲下面幾類:

  • 數據驗證
  • 數據轉換
  • 商業決策
  • 流程流向(工作流邏輯)

上下文在DDD世界中非常重要。上下文的特性決定了領域對象協作及其它運行時因素,比如運用什麼業務規則等。驗證以及其它業務規則往往都是在一個特定的業 務上下文中處理的。這意味着,相同的領域對象在不同的業務上下文中將不得不處理不同的一組業務規則。比如說,通過了貸款審批流程中的承保步驟後,貸款領域 對象的一些屬性(像貸款數額和利率)就不能再改變了。但在貸款剛剛登記並與特定利率關聯的時候,同樣的屬性是可以改變的。

儘管所有的領域特 定業務規則都應該封裝在領域層,但一些應用設計將規則放在了外觀類中,這導致了領域類在業務規則邏輯方面變成了“貧血的”。在小型應用中這可能是可接受的 解決方案,但不推薦將其用於包含複雜業務規則的中大型企業應用。更好的設計方案是把規則放在它們應該在的地方——領域對象中。如果一個業務規則 跨越兩個或兩個以上的實體對象,那麼該規則應該做爲服務類的一部分。

此外,如果我們不在應用中下苦功,往往把業務規則變成代碼裏的一串switch語句。隨着規則變得越來越複雜,開發人員不會願意花費時間去重構代碼,將 switch語句移到更易於管理的設計中。在類中硬編碼複雜的流向或決策規則邏輯會導致類中出現更長的方法、代碼重複、最終僵化的應用設計,長遠來看,這 將成爲維護的噩夢。一個良好的設計是把所有的規則(特別是隨着業務策略的變化而頻繁改變的複雜規則)放到規則引擎(利用規則框架,比如JBoss RulesOpenRulesMandarax)中去,並從領域類中進行調用。

驗證規則通常會用不同的語言實現,比如Javascript、XML、Java代碼,還有其它腳本語言。但由於業務規則的動態特性,RubyGroovy領域特定語言(DSL) 這些腳本語言是定義、管理這些規則更好的選擇。Struts(應用層)、Spring(服務層)和Hibernate(ORM)都有其自己的驗證模塊,我 們可以在這些驗證模塊中對傳入或傳出的數據對象運用驗證規則。在一些情況下,驗證規則還能被處理爲方面,它們可以組合到應用的不同層次中去(比如服務和控 制器)。

在編寫領域類處理業務規則時,緊記單元測試方面是非常重要的。規則邏輯中的任何變化都應該很容易、獨立地單元可測。

示例應用包括一個業務規則集來驗證貸款特性是否都在允許的產品和利率規格內。規則在腳本語言中(Groovy)進行定義,並用於傳遞給FundingService對象的貸款數據。

設計

從設計的角度出發,領域層應該有一個定義清晰的邊界,以避免來自非核心領域層關注點的層的損壞,比如特定供應商的說明、數據過濾、轉換等。領域元素應該設 計爲正確地保存領域狀態和行爲。不同的領域元素會基於狀態和行爲進行不同的結構化。下面的表2展示了領域元素及其包含的內容。

表2. 領域元素及其狀態和行爲

領域元素 狀態/行爲
實體、值對象、聚合 狀態和行爲都有
數據傳輸對象 只有狀態
服務、資源庫 只有行爲

同時包含狀態(數據)和行爲(操作)的實體、值對象、聚合應該有定義清晰的狀態和行爲。同時,該行爲不應該超出對象邊界的範圍。實體應該在作用於本地狀態的用例中完成大部分工作。但它們不應該知道太多無關的概念。

對那些封裝領域對象狀態所需要的屬性來說,好的設計實踐是隻包括這些屬性的getter/setter方法。設計領域對象時,只爲那些能改變的屬性提供setter方法。此外,公有的構造函數應該只含有必需的屬性,而不是包含領域類中所有的屬性。

在大部分用例中,我們並不是真的要去直接改變對象的狀態。所以,代替改變內部狀態的做法是,創建一個帶有已改變狀態的新對象並返回該新對象。這種方法在這些用例中就足夠了,還能降低設計的複雜性。

聚合類對調用者隱藏了協作類的用法。聚合類可用來封裝領域類中複雜的、有侵入性的、狀態依賴的需求。

支持DDD的設計模式

有幾種有助於領域驅動設計和開發的設計模式。下面是這些設計模式的列表:

  • 領域對象(DO)
  • 數據傳輸對象(DTO)
  • DTO組裝器
  • 資源庫:資源庫包含領域爲中心的方法,並使用DAO與數據庫交互。
  • 泛型DAO
  • 時態模式(Temporal Patterns):這些模式給豐富的領域模型添加了時間維。Bitemporal框架基於Martin Fowler的時態模式,爲處理領域模型中的雙時態問題提供了設計方法。核心的領域對象及其雙時態屬性能用ORM產品持久化,比如Hibernate。

在DDD中應用的其它設計模式還包括策略模式、外觀模式和工廠模式。Jimmy Nilsson在他的裏討論了工廠模式,認爲它是一種領域模式。

DDD反模式

在最佳實踐和設計模式的反面,架構師和開發人員在實現領域模型時還應該提防一些DDD的壞氣味。由於這些反模式,領域層在應用架構中成爲最不重要的部分,外觀類反而在模型中承擔了更重要的責任。下面是一些反模式:

  • 貧血的領域對象
  • 重複的DAO
  • 肥服務層:服務類在這裏最終會包含所有的業務邏輯。
  • 依戀情結(Feature Envy):這是Martin Fowler在他關於重構的中提到的典型的壞氣味,在該反模式中,一個類的方法對屬於其它類的數據太過念念不忘。

數據訪問對象

DAO和資源庫在領域驅動設計中都很重要。DAO是關係型數據庫和應用之間的契約。它封裝了Web應用中的數據庫CRUD操作細節。另一方面,資源庫是一個獨立的抽象,它與DAO進行交互,並提供到領域模型的“業務接口”。

資源庫使用領域的通用語言,處理所有必要的DAO,並使用領域理解的語言提供對領域模型的數據訪問服務。

DAO方法是細粒度的,更接近數據庫,而資源庫方法的粒度粗一些,而且更接近領域。此外,一個資源庫類中能注入多個DAO。資源庫和DAO能防止解耦的領域模型去處理數據訪問和持久化細節。

領域對象應該只依賴於資源庫接口。這就是爲什麼是注入資源庫、而不是DAO會產生一個更規則的領域模型的原因。DAO類不能由客戶端(服務和其它的消費者類)直接調用。客戶端應該始終調用領域對象,領域對象再調用DAO將數據持久化到數據存儲中。

處理領域對象之間的依賴關係(比如實體及其資源庫之間的依賴關係)是開發人員經常遇到的典型問題。解決這個問題通常的設計方案是讓服務類或外觀類直接調用 資源庫,在調用資源庫的時候返回實體對象給客戶端。該設計最終導致前面提到的貧血領域模型,其中外觀類會開始堆積更多的業務邏輯,而領域對象則成爲單純的 數據載體。好的設計是利用DI和AOP技術將資源庫和服務注入到領域對象中去。

示例應用在實現貸款處理領域模型時遵循了這些設計原則。

持久化

持久化是一個基礎設施方面,領域層應該與其解耦。JPA通過對類隱藏持久化實現的細節,提供了這一抽象。它由註解推動,所以不需要XML映射文件。但同時,表名和列名嵌在代碼中,在某些情況下可能並不是一個靈活的解決辦法。

使用提供數據網格解決方案的網格計算產品,比如Oracle的Coherence、WebSphere的Object Grid、GigaSpaces,開發人員在建模和設計業務領域時,完全不需要考慮RDBMS。數據庫層用內存對象/數據網格的形式從領域層抽象出來。

緩存

在我們討論領域層的狀態(數據)時,我們不得不談到緩存問題。經常訪問的領域數據(比如抵押貸款處理應用中的產品和利率)很值得緩存起來。緩存能提高性能,減少數據庫服務器的負載。服務層很適合緩存領域狀態。TopLinkHibernate這些ORM框架也提供數據緩存。

貸款處理示例應用使用JBossCache框架來緩存產品和利率詳情,以減少數據庫調用、提高應用性能。

事務管理

對保持數據完整性、整體提交或回滾UOW(工作單元模式)來說,事務管理是很重要的。應該在應用架構層的哪裏處理事務一直存在爭議。交叉實體的事務(在同一UOW中跨越多個領域對象)也影響在哪裏處理事務這一設計決策。

一些開發人員傾向於在DAO類中管理事務,這是一個欠佳的設計。該設計導致過細粒度的事務控制,對那些事務跨越多個領域對象的用例來說,這種事務控制沒有 靈活性。服務類應該處理事務;即使事務跨越多個領域對象,服務類也能處理事務,因爲在大多數用例中,是服務類在處理控制流。

示例應用中的FundingServiceImpl類處理資金申請的事務,通過調用資源庫執行多個數據庫操作,並在單一事務中提交或回滾所有的數據庫變化。

數據傳輸對象

領域對象模型在結構上與從業務服務接收或發送的消息不兼容,在這樣一種SOA環境中,DTO就是設計中很重要的一部分。消息通常都在XML模式定義文檔 (XSD)中定義和維護,從XSD編寫(或代碼生成)DTO對象,並在領域和SOA服務層之間使用它們來傳輸數據(消息)是一種普遍的做法。在分佈式應用 中,將來自於一個或多個領域對象中的數據映射到DTO中會成爲必然的弊端,因爲從性能和安全角度出發,跨越網絡發送領域對象是不實際的。

從DDD的角度來看,DTO還有利於維護服務層和UI層之間的縫隙,其中DO用於領域層和服務層,DTO用於表現層。

Dozer框架用於將一或多個領域對象組裝爲一個DTO對象。它是雙向的,將領域對象轉換爲DTO的時候,它會保存大量備用的代碼和時限,反之亦然。DO和DTO之間的雙向映射有利於消除“DO到DTO”和“DTO到DO”各自的轉換邏輯。該框架還能正確處理類型和數組的轉換。

示例應用在資金處理申請到來時,利用Dozer映射文件(XML)將FundingRequestDTO對象劃分成爲Loan、Borrower、 FundingRequest實體對象。在返回給客戶端時,映射同樣負責將來自實體的資金響應數據聚合到單一的DTO對象中。

DDD實現框架

像Spring、Real Object Oriented(ROO)、Hibernate和Dozer這些框架都有助於設計並實現領域模型。支持DDD實現的其它框架有Naked ObjectsRuby On RailsGrails,以及Spring Modules XT Framework

Spring負責實例化,並將服務、工廠和資源庫這些領域類聯接在一起。它還使用@Configurable註解將服務注入實體。該註解是Spring特有的,所以完成這一注入的其它選擇是使用諸如Hibernate攔截器的東西。

ROO是建立在觀點“領域第一,基礎設施第二”之上的DDD實現框架。開發該框架是爲了減少Web應用開發中模式的模板編碼。利用ROO時,我們定義領域模型,接着框架(基於Maven Archetypes)爲模型-視圖-控制器(MVC)、DTO、業務層外觀和DAO層生成代碼。它也能爲單元測試和集成測試生成stubs。 

ROO有幾個非常實用的實現模式。比如說,它區分處理屬性的狀態、使用屬性級訪問的持久層、只反映必需屬性的公有構造函數。

開發

沒有實際的實現,模型就沒有用處。實現階段應該儘可能多地自動化完成開發任務。爲了看看什麼任務能自動完成,讓我們看看涉及領域模型的一個典型用例。下面是用例的步驟列表:

輸入請求:

  • 客戶端調用外觀類,以XML文檔(XSD兼容的)的方式發送數據;外觀類爲UOW初始化一個新的事務。
  • 驗證輸入的數據。驗證包括基本驗證(基本的/數據類型/屬性級檢查)和業務驗證。如果有任何的驗證錯誤,拋出適當的異常。
  • 將描述轉換爲代碼(以成爲簡單的領域)。
  • 改變數據格式,以成爲簡單的領域模型。
  • 進行所有的屬性分割(比如,在客戶實體對象中,將客戶姓名分成名字和姓)。
  • 把DTO拆分爲一或多個領域對象。
  • 持久化領域對象的狀態。

輸出響應:

  • 從數據存儲中獲取領域對象的狀態。
  • 如果必要,緩存狀態。
  • 將領域對象組裝爲對應用有利的數據對象(DTO)。
  • 進行所有的數據元素合併或分離(比如結合名字和姓,組成單一的客戶姓名屬性)。
  • 將代碼轉換爲描述。
  • 必要時改變數據格式,以處理客戶端數據使用的要求。
  • 如果有必要,緩存DTO的狀態。
  • 事務提交(如果有錯誤則回滾),退出控制流。

下表顯示了應用中不同的對象,這些對象將一個層的數據傳到另一個層。

表3. 應用層間的數據流向

起點對象 終點對象 框架
DAO 數據庫表 DO Hibernate
領域委託      DO DTO Dozer
數據傳輸 DTO XML JAXB

正如你所看到的,相同的數據以不同形式(DO、DTO、XML等)在應用架構中傳遞的層並不多。大部分持有數據的這些對象(Java或XML),還有像 DAO、DAOImpl、DAOTest這些類實際上都是基礎設施。這些有模板代碼和結構的類、XML文件都很適合代碼生成。

代碼生成

ROO這樣的框架還爲新項目創建了一個標準、一致的項目模板(使用Maven插件)。使用預先生成的項目模板,我們可以實現目錄結構的一致性,其中存放源碼、測試類、配置文件,以及對內部和外部(第三方)組件庫的依賴關係。

典型的企業軟件應用所需的種種類和配置文件時,其數量之多令人望而生畏。代碼生成是解決該問題的最好辦法。代碼生成工具通常使用某類模板框架來定義模板,或是代碼生成器能從中生成代碼的映射。Eclipse建模框架(EMF)的幾個子項目有助於Web應用項目需要的各種工件的代碼生成。模型驅動架構(MDA)工具,比如AndroMDA,都利用EMF在架構模型的基礎上生成代碼。

說到在領域層編寫委託類,我看到開發人員手動編寫這些類(大多是從無到有地寫完第一個,接着用“複製並粘貼”的模式來爲其它的領域對象創建所需的委託 類)。由於這些類大部分都是領域類的外觀,它們很適合代碼生成。代碼生成是長遠的解決辦法,儘管建立並測試代碼生成器(引擎)增加了初期的投入(代碼量和 時間)。

對生成的測試類來說,一個好的選擇就是在需要進行單元測試的主類中,爲帶有複雜業務邏輯的方法創建抽象方法。這樣,開發人員能繼承生成的測試基類,然後實現不能自動生成的自定義業務邏輯。同樣,這個方法也適用於任何有不能自動創建測試邏輯的測試方法。

對編寫代碼生成器來說,腳本語言是一個更好的選擇,因爲它們開銷少,還支持模板創建和自定義選項。如果我們在DDD項目中充分利用代碼生成,我們只需要從無到有地編寫少量的代碼。必須從無到有進行創建的工件有:

  • XSD
  • 領域對象
  • 服務

一旦我們定義了XSD和Java類,我們可以生成下列全部或大部分的類和配置文件:

  • DAO接口和實現類
  • 工廠
  • 資源庫
  • 領域代理(如果有必要)
  • 外觀(包括EJB和WebService類)
  • DTO
  • 上述類的單元測試(包括測試類和測試數據)
  • Spring配置文件

表4列出了Web應用架構中不同的層,以及那些層中能生成什麼工件(Java類或XML文件)。

表4. DDD實現項目中的代碼生成

層/功能 模式 你寫的代碼 生成的代碼 框架
數據訪問 DAO/資源庫    DAO接口,
DAO實現類,
DAOTest,
測試種子數據
Unitils,
DBUnit
領域 DO 領域類 DomainTest   
持久化 ORM 領域類 ORM映射,
ORM映射測試
Hibernate,
ORMUnit
數據傳輸 DTO XSD DTO JAXB
DTO組裝 組裝 映射 DO-DTO映射文件 Dozer
委託 業務委託      DO到DTO的轉換代碼      
外觀    外觀    遠程服務,
EJB,
Web Service
控制器 MVC 控制器映射文件 Struts/Spring MVC   
表示層 MVC 視圖配置文件 Spring MVC  

委託層是唯一同時理解領域對象和DTO的層。其它層,例如持久層,不應該察覺到DTO。

重構

重構就是改變或調整應用代碼,但不修改應用的功能或行爲。重構可以是設計相關的,也可以是代碼相關的。設計重構是爲了不斷完善模型、重構代碼來提升領域模型。

由於重構的迭代性和領域建模不斷演進的性質,重構在DDD項目中發揮着重要作用。將重構任務集成到項目中的方法之一是在項目的每次迭代中添加重構環節,重構結束之後纔算完成迭代。理想情況下,每項開發任務之前和之後都應該進行重構。

進行重構應該有嚴格的規定。結合使用重構、CI和單元測試,以確保代碼變化不會破壞任何功能,同時,代碼的變化要有助於以後的代碼和性能改進。

自動化測試在重構應用代碼中發揮着至關重要的作用。沒有良好的自動化測試和測試驅動開發(TDD)實踐,重構可能會產生反面的效果,因爲沒有自動化的方式去驗證作爲重構一部分的設計和代碼並變化沒有改變行爲、或破壞功能。

Eclipse這 樣的工具有助於用迭代的方式和作爲開發一部分的重構來實現領域模型。Eclipse有一些功能,比如把一個方法提取或移動到不同的類中,或將一個方法下推 到子類中。也有幾個Eclipse代碼分析插件有助於處理代碼依賴關係、識別DDD反模式。我做項目的設計和代碼審查時,都是依靠插件JDependClassycleMetrics來評估應用中領域和其它模塊的質量。

Chris Richardson談到運用代碼重構,以使用Eclipse提供的重構功能將過程設計轉變爲一個OO設計。

單元測試/持續集成

我們剛纔談到的目標之一是領域類應該(在最初的開發階段,以及隨後重構已有代碼時)單元可測,而不過多依賴於容器或其它基礎設施代碼。TDD方法有助於團 隊儘早地找出任何設計問題,並有助於驗證代碼與領域模型在保持一致。DDD對測試先行開發來說是很理想的,因爲狀態和行爲都包含在領域類中,而且單獨測試 它們應該是容易的。測試領域模型的狀態和行爲,又不太過關注於數據訪問或持久化的實現細節是很重要的。

單元測試框架,比如JUnit或TestNG,都是實現和處理領域模型很棒的工具。其它測試框架,像DBUnit和Unitils,也可用來測試領域層,尤其是把測試數據注入到DAO類中。對在單元測試類中增加測試數據來說,這將大大減少編寫額外的代碼。

模擬對象(Mock objects)同樣有利於單獨測試領域對象。但是在領域層不要濫用模擬對象是很重要的。如果有其他測試領域類的簡單方法,你應該使用這些方法來代替使用 模擬對象。比如說,如果你能使用真實的後端DAO類(而不是模擬的DAO實現)和內存HSQL數據庫(而不是真實的數據庫)測試一個實體類,能使領域層單 元測試運行得更快,而運行得更快正好是使用模擬對象潛在的主要想法。這樣,你將能測試領域對象之間的協作(交互),以及它們之間交換的狀態(數據)。使用 模擬對象,我們則只能測試領域對象之間的交互。

一旦開發任務完成,所有在開發階段創建的單元測試和集成測試(不管有沒有使用TDD做法)都將成爲自動化測試套件的一部分。這些測試用應該經常進行維護,並經常在本地或更高一級的開發環境中執行,以便找出新的代碼變化是否在領域類中引入了Bug。

Eric Evans在他的中提到了CI,他說CI應該始終運用在界定的上下文中,應該包括人和代碼的同步。像CruiseControlHudson這些CI工具可用來建立一個自動化構建和測試的環境,來運行應用構建腳本(使用Ant或Maven這些構建工具創建)從SCM倉庫中(像CVSSubversion等)檢出代碼,編譯領域類(以及應用中的其它類),並在沒有構建錯誤的情況下自動運行所有的測試(單元測試和集成測試)。CI工具還可以設置在有任何構建或測試錯誤時(通過E-mail或RSS Feeds)通知項目團隊。

部署

領域模型絕對不會是靜態的;在項目生命週期中,它們會隨着業務需求的演變、新項目中新需求的提出而發生變化。此外,隨着你開發和實現領域模型,你能不斷學習和提高,而且你也想在已有的模型中運用新的知識。

打包、部署領域類的時候,隔離很關鍵。因爲領域層依賴於DAO層的一面,而服務外觀層又依賴於DAO層的另一面(參見圖2-應用架構圖),所以這些領域類打包、部署爲一或多個模塊來處理依賴關係很有意義。

DI、AOP和工廠這些設計模式在設計階段減少了對象之間的耦合,並使應用模塊化;OSGi(以前被稱爲開放服務網關規範)則在運行時處理模塊化。OSGi正在成爲打包、發佈企業應用的標準機制。它能很好地處理模塊之間的依賴關係。我們還能用OSGi來進行領域模型的版本處理。

我們可以把DAO類打包到一個OSGi的Bundle(DAO Bundle)中,把服務外觀類打包到另一個Bundle(服務Bundle)中,所以DAO或服務實現進行了修改,或是部署了應用的不同版本,由於 OSGi,應用都不需要重啓。如果我們爲了向後兼容,必須支持某些領域對象已有的版本和新的版本,那我們也可以部署相同領域類的兩個不同版本。

爲了利用OSGi的能力,應用對象在消費之前(即在客戶端能查找到它們之前),應該在OSGi平臺中進行註冊。這意味着我們必須使用OSGi的API進行註冊,我們還必須處理使用OSGi容器啓動和通知服務時的失敗場景。Spring Dynamic Modules框架對該領域很有利,它允許在應用中導出或導入任何對象類型,而不改變任何代碼。

Spring DM還提供測試類,以在容器外運行OSGi集成測試。比如說,能從IDE中直接用AbstractOsgiTests運行集成測試。設置由測試基礎設施來處理,所以我們不需要爲測試編寫MANIFEST.MF文件,或者進行任何的打包或部署。該框架支持大部分目前可用的OSGi實現(EquinoxKnopflerfishApache 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安全
  • JAXB(用於封送處理和取消封送處理數據的Spring-WS)
  • Spring Testing(用於單元測試和集成測試)
  • DBUnit
  • Spring Dynamic Modules

示例應用中的領域類利用Equinox和Spring DM框架部署爲OSGi模塊。下表顯示了示例應用的模塊打包細節。

表5. 打包、部署細節

部署工件名稱 模塊內容 Spring配置文件
客戶端/控制器 loanapp-controller.jar 控制器,客戶端代理類 LoanAppContext-Controller.xml
外觀 loanapp-service.jar 外觀(遠程)服務,服務代理類,XSD LoanAppContext-RemoteServices.xml
領域 loanapp-domain.jar 領域類、DAO,通用的DTO LoanAppContext-Domain.xml, LoanAppContext-Persistence.xml
框架 loanapp-framework.jar 框架,實用工具,監視(JMX)類,方面 LoanAppContext-Framework.xml, LoanAppContext-Monitoring.xml, LoanApp-Aspects.xml

結論

DDD是一個功能強大的概念,只要團隊接受了DDD的培訓,並開始運用“領域第一,基礎設施第二”的觀點,它就會改變建模者、架構師、開發人員和測試人員 思考軟件的方式。由於領域建模、設計和實現中會涉及具有不同背景和專長領域的不同利益相關方(來自IT和業務單位),引用Eric Evans的說法,“不要弄混設計觀點(DDD)和有助於我們完成它的技術工具箱(OOP、DI、AOP)之間的界限”。

前進中的新領域

本節涵蓋了一些新出現的、影響DDD設計和開發的方法。這些概念中的一些仍在不斷髮展,觀察它們將如何影響DDD也很有意思。

在領域模型標準的治理、策略實施,以及實現的最佳實踐中,實施Architecture Rules和契約式設計起到了重要作用。Ramnivas談到了利用Aspects來強制僅通過工廠創建資源庫對象;這是在設計領域層時經常被違背的規則。

領域特定語言(DSL)和業務自然語言(BNL)近幾年來正得到越來越多的關注。人們可以在領域類中使用這些語言表達業務邏輯。BNL可以用來保存業務規 範,記錄業務規則,還能作爲可執行代碼,從這種意義上來說,BNL是非常強大的。還能用它們創建測試用例,來驗證系統是否如預期的那樣運轉。

行爲驅動開發(BDD) 是最近被討論的另一個有趣概念。通過提供跨越業務和技術之間鴻溝的通用詞彙(通用語言),BDD有利於將開發集中在有優先次序、可驗證的商業價值的發佈 上。通過利用側重於系統行爲方面的術語,而不是單單着眼於測試,BDD引導開發人員將TDD背後的真正價值最大程度地發揮出來。如果正確實踐的話,BDD 可以成爲DDD很好的補充,BDD概念會對領域對象的開發產生積極的影響;畢竟領域對象就是對狀態和行爲的封裝。

事件驅動的體系架構(EDA) 是能在領域驅動設計中發揮作用的另一個領域。比如說,在領域對象實例中通知任何狀態變化的事件模型將有助於處理後事件(post-event)處理任務, 在領域對象的狀態改變時,後事件處理任務就需要被觸發。EDA有利於封裝基於事件的邏輯,將之嵌進領域邏輯的核心。Martin Fowler評述了領域事件設計模式。

資源

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章