由Spring應用的瑕疵談談DDD
Spring 框架已經成為構建企業(yè)級 Java 應用事實上的標準了,眾多的企業(yè)項目都構建在 Spring 項目及其子項目之上,特別是 Java Web 項目,很多都使用了 Spring 并且遵循著 Web、Service、Dao 這樣的分層原則,下層向上層提供服務;不過Petri Kainulainen在其博客中卻指出了眾多 Spring Web 應用的最大瑕疵。
多數有經驗的程序開發(fā)者都應該聽說過DDD,并且嘗試過將其應用在自己的項目中。不知你是否遇到過這樣的場景:你創(chuàng)建了一個資源庫(Repository),但一段時間之后發(fā)現這個資源庫和傳統的DAO越來越像了,你開始反思自己的實現方式是正確的嗎?或者,你創(chuàng)建了一個聚合,然后發(fā)現這個聚合是如此的龐大,它為什么引用了如此多的對象,難道又是我做錯了嗎?
本文將會談談有關領域驅動設計,和領域驅動設計中使用貧血、失血和充血模型。
Spring 應用的瑕疵
現在大部分應用Spring框架的Java Web應用都相當關注單一職責原則和關注分離原則,但是在此之上卻誕生了一些不太好的反模式和設計原則,比如:
- 領域模型對象只是用來存儲應用的數據(領域模型使用了貧血模型這種反模式)。
- 業(yè)務邏輯位于服務層中,管理域對象的數據。
- 在服務層中,應用的每個實體對應一個服務類。
使用 Spring 框架構建應用的開發(fā)者很樂于談論依賴注入的好處。但遺憾的是,他們很多人并沒有在其應用中很好地利用其優(yōu)勢,如單一職責原則和關注分離原則。如果仔細看看基于 Spring 的 Web 應用,你會發(fā)現很多都是使用如下這些常見且錯誤的設計原則來實現的:
這類設計原則的應用非常廣泛,我現在所在的Java Web項目就是使用這樣的設計原則進行架構設計的,基本都是常見的三層或多層架構,他們大概是什么樣的呢?
- Web層(俗稱展現層吧,Presentation Layer):接收用戶輸入,將數據傳至服務層;
- 服務層(Service Layer,可以叫Business Logic Layer):事務邊界,處理業(yè)務邏輯、權限管理與授權,并與存儲層通信;
- 存儲層(Data access layer):與數據庫進行通信,對數據進行持久化。
但是發(fā)現什么沒有?問題出在了服務層,他承受了太多的職責,像事務管理、業(yè)務邏輯、權限檢查等等,這違反了單一職責原則和關注分離原則,并且產生了大量的依賴和循環(huán)依賴。當業(yè)務復雜度上升時,服務層所包含的代碼將會非常龐大和復雜,直接導致了測試成本的上升。服務層主要有兩個問題:
- 應用的業(yè)務邏輯來自于服務層。
業(yè)務邏輯散落在服務層。如果需要查看某個業(yè)務規(guī)則是如何實現的,我們需要先找到它。此外,如果有多個服務類都需要相同的業(yè)務規(guī)則,那么會將這個業(yè)務規(guī)則從一個服務復制到另一個服務中,大量的代碼重復。
- 每個領域模型類在服務層中都有一個服務類。
這違背了單一職責原則:單一職責原則表明每個類都應該只有一個職責,這個職責應該完全被這個類所封裝。它的所有服務都應該與這個職責保持一致。
如何改善現狀,下面具體介紹領域驅動設計的相關概念和實施策略。
領域驅動設計(DDD)
DDD總體結構分為四層: Infrastructure(基礎實施層),Domain(領域層),Application(應用層),Interfaces(表示層,也叫用戶界面層或是接口層),各個層面的作用下面介紹。
- 用戶界面(表現層):負責給用戶展示信息,并解釋用戶命令。
- 應用層:該層協調應用程序的活動。不包括任何業(yè)務邏輯,不保存業(yè)務對象的狀態(tài),但能保存應用程序任務過程的狀態(tài)。
- 領域層:這一層包括業(yè)務領域的信息。業(yè)務對象的狀態(tài)在這里保存。業(yè)務對象的持久化和它們的狀態(tài)可能會委托給基礎設施層。
- 基礎設施層:對其它層來說,這一層是一個支持性的庫。它提供層之間的信息傳遞,實現業(yè)務對象的持久化,包含對用戶界面層的支持性庫等。
基本概念
實體(Entity)
當一個對象由其標識(而不是屬性)區(qū)分時,這種對象稱為實體(Entity)。比如當兩個對象的標識不同時,即使兩個對象的其他屬性全都相同,我們也認為他們是兩個完全不同的實體。
值對象(Value Object)
當一個對象用于對事物進行描述而沒有唯一標識時,那么它被稱作值對象。因為在領域中并不是任何時候一個事物都需要有一個唯一的標識,也就是說我們并不關心具體是哪個事物,只關心這個事物是什么。比如下單流程中,對于配送地址來說,只要是地址信息相同,我們就認為是同一個配送地址。由于不具有唯一標示,我們也不能說"這一個"值對象或者"那一個"值對象。
領域服務(Domain Service)
一些重要的領域行為或操作,它們不太適合建模為實體對象或者值對象,它們本質上只是一些操作,并不是具體的事物,另一方面這些操作往往又會涉及到多個領域對象的操作,它們只負責來協調這些領域對象完成操作而已,那么我們可以歸類它們?yōu)轭I域服務。它實現了全部業(yè)務邏輯并且通過各種校驗手段保證業(yè)務的正確性。同時呢,它也能避免在應用層出現領域邏輯。理解起來,領域服務有點facade的味道。
聚合及聚合根(Aggregate,Aggregate Root)
聚合是通過定義領域對象之間清晰的所屬關系以及邊界來實現領域模型的內聚,以此來避免形成錯綜復雜的、難以維護的對象關系網。聚合定義了一組具有內聚關系的相關領域對象的集合,我們可以把聚合看作是一個修改數據的單元。
聚合根屬于實體對象,它是領域對象中一個高度內聚的核心對象。(聚合根具有全局的唯一標識,而實體只有在聚合內部有唯一的本地標識,值對象沒有唯一標識,不存在這個值對象或那個值對象的說法)
若一個聚合僅有一個實體,那這個實體就是聚合根;但要有多個實體,我們就要思考聚合內哪個對象有獨立存在的意義且可以和外部領域直接進行交互。
工廠(Factory)
DDD中的工廠也是一種封裝思想的體現。引入工廠的原因是:有時創(chuàng)建一個領域對象是一件相對比較復雜的事情,而不是簡單的new操作。工廠的作用是隱藏創(chuàng)建對象的細節(jié)。事實上大部分情況下,領域對象的創(chuàng)建都不會相對太復雜,故我們僅需使用簡單的構造函數創(chuàng)建對象就可以。隱藏創(chuàng)建對象細節(jié)的好處是顯而易見的,這樣就可以不會讓領域層的業(yè)務邏輯泄露到應用層,同時也減輕應用層負擔,它只要簡單調用領域工廠來創(chuàng)建出期望的對象就可以了。
倉儲(Repository)
資源倉儲封裝了基礎設施來提供查詢和持久化聚合操作。這樣能夠讓我們始終關注在模型層面,把對象的存儲和訪問都委托給資源庫來完成。它不是數據庫的封裝,而是領域層與基礎設施之間的橋梁。DDD 關心的是領域內的模型,而不是數據庫的操作。
DDD設計
DDD 概念理解起來有點抽象,這個有點像設計模式,感覺很有用,但是自己開發(fā)的時候又不知道怎么應用到代碼里面,或者生搬硬套后自己看起來都很別扭。DDD的戰(zhàn)略設計主要包括領域/子域、通用語言、限界上下文和架構風格等概念。
領域和子域
現實世界中,領域包含了問題域和解系統。一般認為軟件是對現實世界的部分模擬。在DDD中,解系統可以映射為一個個限界上下文,限界上下文就是軟件對于問題域的一個特定的、有限的解決方案。
在日常開發(fā)中,我們通常會將一個大型的軟件系統拆分成若干個子系統。這種劃分有可能是基于架構方面的考慮,也有可能是基于基礎設施的。但是在DDD中,我們對系統的劃分是基于領域的,也即是基于業(yè)務的。
限界上下文
一個由顯示邊界限定的特定職責。領域模型便存在于這個邊界之內。在邊界內,每一個模型概念,包括它的屬性和操作,都具有特殊的含義。
將一個限界上下文中的所有概念,包括名詞、動詞和形容詞全部集中在一起,我們便為該限界上下文創(chuàng)建了一套通用語言。通用語言是一個團隊所有成員交流時所使用的語言,業(yè)務分析人員、編碼人員和測試人員都應該直接通過通用語言進行交流。
對于上文中提到的各個子域之間的集成問題,其實也是限界上下文之間的集成問題。在集成時,我們主要關心的是領域模型和集成手段之間的關系。比如需要與一個REST資源集成,你需要提供基礎設施(比如Spring 中的RestTemplate),但是這些設施并不是你核心領域模型的一部分,你應該怎么辦呢?答案是防腐層,該層負責與外部服務提供方打交道,還負責將外部概念翻譯成自己的核心領域能夠理解的概念。當然,防腐層只是限界上下文之間眾多集成方式的一種,另外還有共享內核、開放主機服務等,具體細節(jié)請參考《實現領域驅動設計》原書。限界上下文之間的集成關系也可以理解為是領域概念在不同上下文之間的映射關系,因此,限界上下文之間的集成也稱為上下文映射圖。
小結
本文通過Spring Web應用的瑕疵引出改善的措施,隨后介紹了領域驅動開發(fā)的相關概念和設計策略。在前面講了這么多概念,想必讀者一定有了解如何落地領域驅動設計的沖動。筆者將在下一篇文章介紹領域模型的幾種類型和DDD的具體實踐案例。