(轉)領域驅動設計和開發實戰之一

netloser 14年前發布 | 3K 次閱讀 中興 USB Entity Framework Windows Office 15

背景

領域驅動設計(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對象的貸款數據

 本文由用戶 netloser 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!