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