(轉)領域驅動設計和開發實戰之二
設計
從設計的角度出發,領域層應該有一個定義清晰的邊界,以避免來自非核心領域層關注點的層的損壞,比如特定供應商的說明、數據過濾、轉換等。領域元素應該設 計為正確地保存領域狀態和行為。不同的領域元素會基于狀態和行為進行不同的結構化。下面的表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。數據庫層用內存對象/數據網格的形式從領域層抽象出來。
緩存
在我們討論領域層的狀態(數據)時,我們不得不談到緩存問題。經常訪問的領域數據(比如抵押貸款處理應用中的產品和利率)很值得緩存起來。緩存能提高性能,減少數據庫服務器的負載。服務層很適合緩存領域狀態。TopLink和Hibernate這些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 Objects、Ruby On Rails、Grails,以及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反模式。我做項目的設計和代碼審查時,都是依靠插件JDepend、Classycle和Metrics來評估應用中領域和其它模塊的質量。
Chris Richardson談到運用代碼重構,以使用Eclipse提供的重構功能將過程設計轉變為一個OO設計。
單元測試/持續集成
我們剛才談到的目標之一是領域類應該(在最初的開發階段,以及隨后重構已有代碼時)單元可測,而不過多依賴于容器或其它基礎設施代碼。TDD方法有助于團 隊盡早地找出任何設計問題,并有助于驗證代碼與領域模型在保持一致。DDD對測試先行開發來說是很理想的,因為狀態和行為都包含在領域類中,而且單獨測試 它們應該是容易的。測試領域模型的狀態和行為,又不太過關注于數據訪問或持久化的實現細節是很重要的。
單元測試框架,比如JUnit或TestNG,都是實現和處理領域模型很棒的工具。其它測試框架,像DBUnit和Unitils,也可用來測試領域層,尤其是把測試數據注入到DAO類中。對在單元測試類中增加測試數據來說,這將大大減少編寫額外的代碼。
模擬對象(Mock objects)同樣有利于單獨測試領域對象。但是在領域層不要濫用模擬對象是很重要的。如果有其他測試領域類的簡單方法,你應該使用這些方法來代替使用 模擬對象。比如說,如果你能使用真實的后端DAO類(而不是模擬的DAO實現)和內存HSQL數據庫(而不是真實的數據庫)測試一個實體類,能使領域層單 元測試運行得更快,而運行得更快正好是使用模擬對象潛在的主要想法。這樣,你將能測試領域對象之間的協作(交互),以及它們之間交換的狀態(數據)。使用 模擬對象,我們則只能測試領域對象之間的交互。
一旦開發任務完成,所有在開發階段創建的單元測試和集成測試(不管有沒有使用TDD做法)都將成為自動化測試套件的一部分。這些測試用應該經常進行維護,并經常在本地或更高一級的開發環境中執行,以便找出新的代碼變化是否在領域類中引入了Bug。
Eric Evans在他的書中提到了CI,他說CI應該始終運用在界定的上下文中,應該包括人和代碼的同步。像CruiseControl和Hudson這些CI工具可用來建立一個自動化構建和測試的環境,來運行應用構建腳本(使用Ant或Maven這些構建工具創建)從SCM倉庫中(像CVS、Subversion等)檢出代碼,編譯領域類(以及應用中的其它類),并在沒有構建錯誤的情況下自動運行所有的測試(單元測試和集成測試)。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實現(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安全
- 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出版社