你的分層架構(gòu)還好嗎?
分層架構(gòu),不就是建文件夾的藝術(shù)嗎?
注:本文更適用于中大型項(xiàng)目,小項(xiàng)目開心就好了。因?yàn)闀r代的原因,對部分詞匯描述可能不是那么準(zhǔn)確,歡迎指正。
當(dāng)我們開始一個新的項(xiàng)目,我們就開始創(chuàng)建一個個折文件夾。哦,不對,那我們在做分層架構(gòu)設(shè)計。架構(gòu)最后落到現(xiàn)有的計算機(jī)操作系統(tǒng)上,其的展示形式是分層架構(gòu)。畢竟,硅基不如碳基。
可是呢,為什么我們要做分層架構(gòu)設(shè)計呢?通過層(Layer)來隔離不同的關(guān)注點(diǎn)。
So,我要開始瞎扯了。
基本思想:關(guān)注點(diǎn)分離,劃分邊界
注:三層架構(gòu)(controller-service-model)并非等于于 MVC 架構(gòu)模式。對于其的錯誤等同,導(dǎo)致了架構(gòu)上的一系列錯誤。
問題:落后的三層架構(gòu)
過去,我總以為對于大部分項(xiàng)目來說,三層分層架構(gòu)之外的部分是大泥球,即隨意化的代碼組織方式。然而,我發(fā)現(xiàn)對于大部分的項(xiàng)目來說,三層分層架構(gòu)的 service 也是個大泥球,我忘記了三層分層架構(gòu)的 model 層也是一堆大泥球。Controller 相對好一點(diǎn),但是對于某些項(xiàng)目來說也是個小泥球。
大泥球是指一個隨意化的雜亂的結(jié)構(gòu)化系統(tǒng),只是代碼的堆砌和拼湊,往往會導(dǎo)致很多錯誤或者缺陷。
在今天 DDD + 整潔架構(gòu)流行的今天, 三層分層架構(gòu)已經(jīng)完全不能滿足現(xiàn)有應(yīng)用的需求,甚至看上去一團(tuán)糟糕。它存在這么一些問題:
- 統(tǒng)一管理是魔鬼,如 controller 文件夾下一堆的代碼,到處亂放的 model。
- 缺乏明確的職責(zé)劃分,如 controller 承擔(dān)了 service 的職責(zé)
- 臃腫的 service,和貧血的 model
- 三層分層之后的隨意文件組織方式,如 kafka 等到處亂放的代碼
- ……
可是,為什么會這樣呢?
- 職責(zé)(or 限界上下文)沒有劃分明確和清晰
- model 層存在大量的二義性
- 技術(shù)導(dǎo)向架構(gòu)模式
- ……
于是,我們有了一些基本的解決方案,或者說是套路。
重新定義:消除二義性
當(dāng)我們談?wù)?service 的時候,我們談?wù)摰氖峭粋€ service 嗎?
當(dāng)我們談?wù)?model 的時候,我們談?wù)摰氖峭环N model 嗎?
若對于一個文法的某一句子存在兩棵不同的語法樹,則該文法是二義性文法。
如果有多種不同類型的類,都被放置在 model 包下。那么,你應(yīng)該消除 model 這個包,改為更表意的名稱,如 Entity、* Request、* Response 等等。同理,一旦你們展開對某個名稱的討論時,是時候好好考慮其中的二義性。
最后,你還需要有一個相關(guān)領(lǐng)域的名詞表。
劃分邊界:業(yè)務(wù)導(dǎo)向架構(gòu)
開始之前不得不說的是:
- 微服務(wù)是一種業(yè)務(wù)導(dǎo)向架構(gòu)。
- 微服務(wù)是一種業(yè)務(wù)導(dǎo)向架構(gòu)。
- 微服務(wù)是一種業(yè)務(wù)導(dǎo)向架構(gòu)。
所以,如果你的微服務(wù)劃分出現(xiàn)了不同的幾個技術(shù)維度的服務(wù),那么你需要好好反思一下。
So,為了迎接業(yè)務(wù)導(dǎo)向架構(gòu),我們需要以采用水平 + 垂直架構(gòu)的方式來重新劃分架構(gòu),將各業(yè)務(wù)模板的代碼聚合到各自的業(yè)務(wù)模板中,順便把大量地 util 和 common 內(nèi)聚到服務(wù)中。而它們都基于其它低層模板。
隨后,我們還可以嘗試將單體應(yīng)用拆分到微服務(wù)。
但是,我們都不應(yīng)該依賴于低層模塊,于是就有了……。
關(guān)注點(diǎn)分離:針對接口編程
我們看到了整潔架構(gòu):
在其中有一個非常重要的原則:
依賴倒置原則:高層模塊不應(yīng)該依賴于低層模塊,二者都應(yīng)該依賴于抽象。
這一點(diǎn)在大部分的項(xiàng)目中,已經(jīng)實(shí)踐得相關(guān)的好。畢竟,有各種各樣的 * Service + * ServiceImpl。
除此,為了實(shí)現(xiàn)這樣的目標(biāo),對于采用 DDD 架構(gòu)的應(yīng)用來說,在我們的 domain 層的限界上下文,除了包含自身的 entity、vo 等,它應(yīng)該還帶有 repository 的抽象。這樣一來,我們的 domain 層便不依賴
應(yīng)用分層:DDD 與整潔架構(gòu)
所以,讓我們來看個問題。這是一個在 GitHub 上 star 數(shù)接近 15K 的 Java 語言編寫的開源 CMS 中,某個模塊的代碼目錄:
- ├── cms-admin/
- ├── cms-common/
- ├── cms-dao/
- ├── cms-job/
- ├── cms-rpc-api/
- ├── cms-rpc-service/
- ├── cms-search/
- └── cms-web/
這是一個技術(shù)導(dǎo)向的應(yīng)用架構(gòu)。所以,當(dāng)我要新加一個功能的時候,我需要:
- 在 cms-dao 模塊中加一個 model 和一個 mapper
- 在 cms-rpc-service 模塊中加一個 service
- 在 cms-web 模塊中中加一個 controller
“完美”,沒有什么問題啊。
而隨著時間的推移,你現(xiàn)在已經(jīng)有一個巨大無比的 model 層,修改代碼時需要在不同的模塊跳轉(zhuǎn)。而不能快速修改相關(guān)的代碼。甚至于,你無法采用微服務(wù)架構(gòu),你是一個巨大的單體應(yīng)用。
為了挽救這樣的一個項(xiàng)目,我們不得不嘗試做一些事情。
切割基礎(chǔ)設(shè)施
你的基礎(chǔ)設(shè)施自開發(fā)完成之后,基本不變,而你的業(yè)務(wù)代碼一直在發(fā)生變化。
引起技術(shù)實(shí)現(xiàn)發(fā)生變化的原因與引起領(lǐng)域邏輯發(fā)生變化的原因顯然不同,這就導(dǎo)致基礎(chǔ)設(shè)施和領(lǐng)域邏輯問題會以不同速率發(fā)生變化。——《領(lǐng)域驅(qū)動設(shè)計模式、原理與實(shí)踐》
當(dāng)你來到一個項(xiàng)目一眼看到這么多基礎(chǔ)設(shè)施相關(guān)的目錄結(jié)構(gòu)時:
- ├── controller
- ├── interceptor
- ├── jms
- ├── rocketmq
- ├── schedule
- └── task
有一天,我們又加了一個 Kafka,我們又不新加一個文件夾,而這樣的分層設(shè)計看上去沒有一點(diǎn)組織。然后呢,我們打開目錄的時候,無法快速定位到我們的代碼。
除了從目錄上 infrastructure 包/層,容納相關(guān)的基礎(chǔ)設(shè)施代碼。我們還要考慮到分層上的單一職責(zé),因?yàn)樾枰獎冸x基礎(chǔ)設(shè)施與業(yè)務(wù)代碼的關(guān)系。所以,為了實(shí)現(xiàn) Clean Architecture 的大業(yè),你還需要一層抽象接口,比如你要訪問存儲業(yè)務(wù)相關(guān)的數(shù)據(jù)。那么抽象在你的 domain 中,具體的 RepositoryImpl 實(shí)現(xiàn)是在你的基礎(chǔ)設(shè)施。
離心分離模型
在一個系統(tǒng)中,你會存在這么一些不同的 model:
(PS:部分描述可能不準(zhǔn)確,歡迎指正)
- 與數(shù)據(jù)庫表結(jié)構(gòu)對應(yīng)的 DO( Data Object)/ PO(Persistant Object)。
- 查詢數(shù)據(jù)的 Query、Request。
- 對外傳輸?shù)膶ο螅篋TO( Data Transfer Object)。
- 業(yè)務(wù)層之間的數(shù)據(jù)對象:VO(Value Object) / BO(Business Object)。
- 訪問數(shù)據(jù)庫的:DAO (Data Access Object數(shù)據(jù)訪問對象)。
- 以及我們想要的 DDD 中的實(shí)體 Entity
- 還有其它的 POJO( Plain Ordinary Java Object)
但是它們都是 model,所以它們都被扔到 model 中……,又或者是 bean 中……。導(dǎo)致,你有了一個巨大比的 model 層。
所以,在 DDD 又或者是 Clean Architecture,我們重新命名了不同的模式:
- 使用 Command / Request 作為輸入?yún)?shù)。其中的 Command 模式在完成后需要發(fā)出對應(yīng)的 Event。
- 使用 Response / DTO / Representation 作為返回結(jié)果。
- 對 Entity 大家保持了一致的意見
- 還有 PO / DO 作為作為數(shù)據(jù)庫的存儲模型
- DAO 作為數(shù)據(jù)庫的訪問模型
- ……
不過,其實(shí)你只要不再讓使用 model 和 bean,相似會有更多地收獲。
以領(lǐng)域?yàn)楹诵?,豐富行為
當(dāng)完成了大坨的移動文件夾操作之后,我們來到了最麻煩和復(fù)雜的一部分。
我們需要對領(lǐng)域模型進(jìn)行重新建模,重新規(guī)劃 model 和 service,讓 model 變成了富血模型。也許,你需要一場 Event Storming,才能完成真正意義的事件風(fēng)暴建模。不過,步驟上也不會有太多的差異。
- 重新劃分包。即在保持業(yè)務(wù)不中斷的情況下重構(gòu),以讓新的代碼運(yùn)行在新的架構(gòu)上。
- 分析抽象領(lǐng)域模型
- 編寫 API 測試,保證現(xiàn)有的功能
- 編寫抽象接口,進(jìn)行依賴反轉(zhuǎn)
- 拆分 service 層,重構(gòu)代碼。將行為綁定于是領(lǐng)域?qū)ο笊稀?/li>
其它的情況,還要進(jìn)行 case by case 的分析。
剩下的呢?
共享的業(yè)務(wù)邏輯,可以采用 sharedkernel,或者其它模式來處理。
待繼續(xù)補(bǔ)充。
代碼共用分層:功能內(nèi)聚
創(chuàng)建通用的共享組件導(dǎo)致了一系列問題,比如耦合、協(xié)調(diào)難度和復(fù)雜度增加。
當(dāng)我看到一個個巨大的 common 包時,我開始痛恨 common、 base、 util 這些該死的包,還有它們目錄下統(tǒng)一管理的 bean。我們真的已經(jīng)把它們用爛了,所以你應(yīng)該重新審視一下你的項(xiàng)目代碼。
所以,從這種意義上來說:復(fù)用與低耦合,本身存在一定的互斥關(guān)系。
base 下的 base
過去,我曾經(jīng)重構(gòu)過一個 base 項(xiàng)目的代碼,正是這次重構(gòu)讓我意識到 base 并不是一個好東西。如果在項(xiàng)目中已經(jīng)抽取出了一個 base 模塊,那么這個模塊下是不應(yīng)該存在 base 這樣的業(yè)務(wù)邏輯。而且,base 這個東西導(dǎo)致了一個問題是,只要是共用的東西就會不加思索的扔到 base 中。
你會有一個 base 的包,放著各種抽象接口,但是你需要一個更好的名字,比如 concepts,比如 support。
總之,你不應(yīng)該存在 base 模塊,讓開發(fā)人員思考一下哪去放新的類。
無比臃腫的 bean 和 model
“這本身是怪不得程序員的,要怪就怪該死的 Java 語言。”
轉(zhuǎn)而,我開始考慮一個問題,當(dāng)個包(文件夾)下的文件數(shù)是否不應(yīng)該超過一定的數(shù)量?
如果一個包下的類數(shù),超過一定的范圍,那么我們應(yīng)該考慮是否存在職責(zé)相似的類。
這部分可以參考上一部分的離心分離模型。
什么不是 common
common 這個名字真的很爛,比 base 和 model 更爛。
一旦你從項(xiàng)目中拆出了一個 common 模塊,那只會有一個結(jié)果,你將得到一個 5G 時代的 jar 包。甚至于,你看到有一塊代碼在 IDE 中是灰色的、未使用的,你也不敢輕易去刪除這些代碼。直到有一天,這個 common 包構(gòu)建出來的大小有 10M、20M,而你只需要引用一個 AESUtil 的時候,你才發(fā)現(xiàn)了問題:原本幾十 K 的 hello, world,現(xiàn)在變成了幾十 M。
不要事先創(chuàng)建 common 模塊,你可能不會有這個模塊。
任何的水平分層拆分應(yīng)用,在項(xiàng)目復(fù)雜化的今天都是不靠譜的。
誰用誰管理,而不是覺得是 common 就扔 common 模塊。
它真是個 util 嗎?
哦,不,它是個惡魔,因?yàn)樗?util。
你會往 xxUtil 不加思索地扔入邏輯,正如你會往 common/bean 中扔入所有的 model,直次有一天,你擁有一個巨大無比的 base、common 代碼。
大多數(shù)情況下,所有和業(yè)務(wù)相關(guān)的 Util 都存在一定的問題,如 CaptchaUtil,它要么應(yīng)該劃到自己的上下文中去,要么扔到諸如于 domain/shared 等共享上下文,而不是和其它 util 放到一起。
而諸如 FileUtil、DateUtil、RedisUtil、JdbcUtil 這些都可以說是基礎(chǔ)設(shè)施相關(guān)的部分,它們可以劃到 infrastructure/file 又或者是 infrastructure/date 目錄下,而不是統(tǒng)一的管理這些 util。
如 StackOverflow 的相關(guān)問題所列,我們還有諸如 Coordinator、Builder、Writer、Reader、Handler、Container、Protocol、Target、Converter、Controller、View、Factory、Entity、Bucket 等名稱。
試著干掉 Util,你將收獲更多的類,笑~。
需要個例子?
看看 Spring Framework 的源碼的分層結(jié)構(gòu),如 Spring Orm:
- └── orm
- ├── ObjectOptimisticLockingFailureException.java
- ├── ObjectRetrievalFailureException.java
- ├── hibernate5a/
- ├── jpa/
- └── package-info.java
又或者是 spring-context 下的目錄分層結(jié)構(gòu):
- └── springframework
- ├── cache
- │ ├── annotation
- │ ├── concurrent
- │ ├── config
- │ ├── interceptor
- │ └── support
- ├── context
- │ ├── annotation
- │ ├── config
- │ ├── event
- │ ├── expression
- │ ├── i18n
- │ ├── index
- │ ├── support
- │ └── weaving
它們都在自己的限界上下文內(nèi),維護(hù)自己的 annotaion、bean、support、i18n 等等的包。
分層架構(gòu)重構(gòu)
所以,我們可以嘗試這么去做架構(gòu)重構(gòu)
- 分析、診斷現(xiàn)有項(xiàng)目結(jié)構(gòu)
- 劃分新的分層架構(gòu)
- 功能測試
- 使用抽象解耦依賴
- 進(jìn)行細(xì)粒度的代碼重構(gòu)
- 重新劃分領(lǐng)域服務(wù)
還有嗎?
- 不要預(yù)先設(shè)計,而是定義原則與規(guī)范。
- 以簡單的設(shè)計開始,在生命周期中演進(jìn)架構(gòu)。
- 以多個 common 包,替代統(tǒng)一的 common 包
- TBC。
結(jié)論
那么,我們怎么才能做好分層架構(gòu)呢?
by experience。
哦,不對,DDD 大法好。