由Spring應用的瑕疵談談DDD的概念與應用(一)

Spring 框架已經成爲構建企業級 Java 應用事實上的標準了,衆多的企業項目都構建在 Spring 項目及其子項目之上,特別是 Java Web 項目,很多都使用了 Spring 並且遵循着 Web、Service、Dao 這樣的分層原則,下層向上層提供服務;不過Petri Kainulainen在其博客中卻指出了衆多 Spring Web 應用的最大瑕疵。

多數有經驗的程序開發者都應該聽說過DDD,並且嘗試過將其應用在自己的項目中。不知你是否遇到過這樣的場景:你創建了一個資源庫(Repository),但一段時間之後發現這個資源庫和傳統的DAO越來越像了,你開始反思自己的實現方式是正確的嗎?或者,你創建了一個聚合,然後發現這個聚合是如此的龐大,它爲什麼引用瞭如此多的對象,難道又是我做錯了嗎?

本文將會談談有關領域驅動設計,和領域驅動設計中使用貧血、失血和充血模型。

Spring 應用的瑕疵

現在大部分應用Spring框架的Java Web應用都相當關注單一職責原則和關注分離原則,但是在此之上卻誕生了一些不太好的反模式和設計原則,比如:

  • 領域模型對象只是用來存儲應用的數據(領域模型使用了貧血模型這種反模式)。
  • 業務邏輯位於服務層中,管理域對象的數據。
  • 在服務層中,應用的每個實體對應一個服務類。

使用 Spring 框架構建應用的開發者很樂於談論依賴注入的好處。但遺憾的是,他們很多人並沒有在其應用中很好地利用其優勢,如單一職責原則和關注分離原則。如果仔細看看基於 Spring 的 Web 應用,你會發現很多都是使用如下這些常見且錯誤的設計原則來實現的:

這類設計原則的應用非常廣泛,我現在所在的Java Web項目就是使用這樣的設計原則進行架構設計的,基本都是常見的三層或多層架構,他們大概是什麼樣的呢?

  • Web層(俗稱展現層吧,Presentation Layer):接收用戶輸入,將數據傳至服務層;
  • 服務層(Service Layer,可以叫Business Logic Layer):事務邊界,處理業務邏輯、權限管理與授權,並與存儲層通信;
  • 存儲層(Data access layer):與數據庫進行通信,對數據進行持久化。

但是發現什麼沒有?問題出在了服務層,他承受了太多的職責,像事務管理、業務邏輯、權限檢查等等,這違反了單一職責原則和關注分離原則,並且產生了大量的依賴和循環依賴。當業務複雜度上升時,服務層所包含的代碼將會非常龐大和複雜,直接導致了測試成本的上升。服務層主要有兩個問題:

  • 應用的業務邏輯來自於服務層。

業務邏輯散落在服務層。如果需要查看某個業務規則是如何實現的,我們需要先找到它。此外,如果有多個服務類都需要相同的業務規則,那麼會將這個業務規則從一個服務複製到另一個服務中,大量的代碼重複。

  • 每個領域模型類在服務層中都有一個服務類。

這違背了單一職責原則:單一職責原則表明每個類都應該只有一個職責,這個職責應該完全被這個類所封裝。它的所有服務都應該與這個職責保持一致。

如何改善現狀,下面具體介紹領域驅動設計的相關概念和實施策略。

領域驅動設計(DDD)

DDD總體結構分爲四層: Infrastructure(基礎實施層),Domain(領域層),Application(應用層),Interfaces(表示層,也叫用戶界面層或是接口層),各個層面的作用下面介紹。

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

基本概念

實體(Entity)

當一個對象由其標識(而不是屬性)區分時,這種對象稱爲實體(Entity)。比如當兩個對象的標識不同時,即使兩個對象的其他屬性全都相同,我們也認爲他們是兩個完全不同的實體。

值對象(Value Object)

當一個對象用於對事物進行描述而沒有唯一標識時,那麼它被稱作值對象。因爲在領域中並不是任何時候一個事物都需要有一個唯一的標識,也就是說我們並不關心具體是哪個事物,只關心這個事物是什麼。比如下單流程中,對於配送地址來說,只要是地址信息相同,我們就認爲是同一個配送地址。由於不具有唯一標示,我們也不能說"這一個"值對象或者"那一個"值對象。

領域服務(Domain Service)

一些重要的領域行爲或操作,它們不太適合建模爲實體對象或者值對象,它們本質上只是一些操作,並不是具體的事物,另一方面這些操作往往又會涉及到多個領域對象的操作,它們只負責來協調這些領域對象完成操作而已,那麼我們可以歸類它們爲領域服務。它實現了全部業務邏輯並且通過各種校驗手段保證業務的正確性。同時呢,它也能避免在應用層出現領域邏輯。理解起來,領域服務有點facade的味道。

聚合及聚合根(Aggregate,Aggregate Root)

聚合是通過定義領域對象之間清晰的所屬關係以及邊界來實現領域模型的內聚,以此來避免形成錯綜複雜的、難以維護的對象關係網。聚合定義了一組具有內聚關係的相關領域對象的集合,我們可以把聚合看作是一個修改數據的單元。

聚合根屬於實體對象,它是領域對象中一個高度內聚的核心對象。(聚合根具有全局的唯一標識,而實體只有在聚合內部有唯一的本地標識,值對象沒有唯一標識,不存在這個值對象或那個值對象的說法)

若一個聚合僅有一個實體,那這個實體就是聚合根;但要有多個實體,我們就要思考聚合內哪個對象有獨立存在的意義且可以和外部領域直接進行交互。

工廠(Factory)

DDD中的工廠也是一種封裝思想的體現。引入工廠的原因是:有時創建一個領域對象是一件相對比較複雜的事情,而不是簡單的new操作。工廠的作用是隱藏創建對象的細節。事實上大部分情況下,領域對象的創建都不會相對太複雜,故我們僅需使用簡單的構造函數創建對象就可以。隱藏創建對象細節的好處是顯而易見的,這樣就可以不會讓領域層的業務邏輯泄露到應用層,同時也減輕應用層負擔,它只要簡單調用領域工廠來創建出期望的對象就可以了。

倉儲(Repository)

資源倉儲封裝了基礎設施來提供查詢和持久化聚合操作。這樣能夠讓我們始終關注在模型層面,把對象的存儲和訪問都委託給資源庫來完成。它不是數據庫的封裝,而是領域層與基礎設施之間的橋樑。DDD 關心的是領域內的模型,而不是數據庫的操作。

DDD設計

DDD 概念理解起來有點抽象,這個有點像設計模式,感覺很有用,但是自己開發的時候又不知道怎麼應用到代碼裏面,或者生搬硬套後自己看起來都很彆扭。DDD的戰略設計主要包括領域/子域、通用語言、限界上下文和架構風格等概念。

領域和子域

現實世界中,領域包含了問題域和解系統。一般認爲軟件是對現實世界的部分模擬。在DDD中,解系統可以映射爲一個個限界上下文,限界上下文就是軟件對於問題域的一個特定的、有限的解決方案。

在日常開發中,我們通常會將一個大型的軟件系統拆分成若干個子系統。這種劃分有可能是基於架構方面的考慮,也有可能是基於基礎設施的。但是在DDD中,我們對系統的劃分是基於領域的,也即是基於業務的。

限界上下文

一個由顯示邊界限定的特定職責。領域模型便存在於這個邊界之內。在邊界內,每一個模型概念,包括它的屬性和操作,都具有特殊的含義。

將一個限界上下文中的所有概念,包括名詞、動詞和形容詞全部集中在一起,我們便爲該限界上下文創建了一套通用語言。通用語言是一個團隊所有成員交流時所使用的語言,業務分析人員、編碼人員和測試人員都應該直接通過通用語言進行交流。

對於上文中提到的各個子域之間的集成問題,其實也是限界上下文之間的集成問題。在集成時,我們主要關心的是領域模型和集成手段之間的關係。比如需要與一個REST資源集成,你需要提供基礎設施(比如Spring 中的RestTemplate),但是這些設施並不是你核心領域模型的一部分,你應該怎麼辦呢?答案是防腐層,該層負責與外部服務提供方打交道,還負責將外部概念翻譯成自己的核心領域能夠理解的概念。當然,防腐層只是限界上下文之間衆多集成方式的一種,另外還有共享內核、開放主機服務等,具體細節請參考《實現領域驅動設計》原書。限界上下文之間的集成關係也可以理解爲是領域概念在不同上下文之間的映射關係,因此,限界上下文之間的集成也稱爲上下文映射圖。

小結

本文通過Spring Web應用的瑕疵引出改善的措施,隨後介紹了領域驅動開發的相關概念和設計策略。在前面講了這麼多概念,想必讀者一定有了解如何落地領域驅動設計的衝動。筆者將在下一篇文章介紹領域模型的幾種類型和DDD的具體實踐案例。

訂閱最新文章,歡迎關注我的公衆號

參考

  1. Spring Web 應用的最大瑕疵
  2. 領域驅動設計(DDD)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章