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