背景

領域驅動設計(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評述了領域事件設計模式。

資源

  • 領域驅動設計:軟體核心複雜性應對之道》,Evans Eric著,Addison-Wesley出版社
  • 《領域驅動設計和模式應用》,Jimmy Nilsson著,Addison-Wesley出版社
  • 《重構到模式》,Joshua Kerievsky著,Addison-Wesley出版社