我專案中的程式碼都是如何分層的?
1、背景
說起應用分層,大部分人都會認為這個不是很簡單嘛 就controller,service, mapper三層。看起來簡單,很多人其實並沒有把他們職責劃分開,在很多程式碼中,controller做的邏輯比service還多,service往往當成透傳了,這其實是很多人開發程式碼都沒有注意到的地方,反正功能也能用,至於放哪無所謂唄。這樣往往造成後面程式碼無法複用,層級關係混亂,對後續程式碼的維護非常麻煩。
的確在這些人眼中分層只是一個形式,前輩們的程式碼這麼寫的,其他專案程式碼這麼寫的,那麼我也這麼跟著寫。
但是在真正的團隊開發中每個人的習慣都不同,寫出來的程式碼必然帶著自己的標籤,有的人習慣controller寫大量的業務邏輯,有的人習慣在service中之間呼叫遠端服務,這樣就導致了每個人的開發程式碼風格完全不同,後續其他人修改的時候,一看,我靠這個人寫的程式碼和我平常的習慣完全不同,修改的時候到底是按著自己以前的習慣改,還是跟著前輩們走,這又是個艱難的選擇,選擇一旦有偏差,你的後輩又維護你的程式碼的時候,恐怕就要罵人了。
所以一個好的應用分層需要具備以下幾點:
方便後續程式碼進行維護擴充套件;
分層的效果需要讓整個團隊都接受;
各個層職責邊界清晰。
2、應用分層模型
在專案開發中,一個良好的工程架構是必須的。工程架構就像一個骨架,寫程式碼就是在這個骨架上增添血肉,這個骨架會影響到整體的模組劃分,功能劃分,即會影響到程式碼的解耦和聚合,將會很大程度上決定一個專案寫得好不好。
這裡要分享的是我個人在開發時所採取的工程架構,以及相關的思想。不同的人對於工程架構的理解會不同,實際上也很難分出哪種好,哪種壞,只要符合自己的設計思想,並且能夠有效的進行開發,那就是好的一種架構方式。
2.1、分層
我整體上的思想為《阿里巴巴 Java 開發手冊》中所描述的分層模型。如下:
接下來將自底向上的講解我對各層的理解和設計,還有我自己所增加的層。
2.2、通用工具層
通用工具層其實為對業務無關的,通用的工具類,例如日期處理的工作累,一些資料格式的序列化與反序列化工具。類似於 apache commons 包和 guava 包。
2.3、分層領域模型
領域模型,也就是我們之前常見的各種資料實體,用 DDD 的術語來說,這種在分層模型中的領域模型稱為貧血領域模型。
貧血領域模型只作為資料載體,只有 getter/setter 方法,而不包含業務方法。
對於分層領域模型,會進一步進行劃分規約,主要也是參考自《阿里巴巴 Java 開發手冊》具體如下:
- DO(Data Object) : 資料物件,對資料來源資料的對映,如資料庫表,ElasticSearch 索引的資料結構。所在包一般命名為 data 。
- DTO(Data Transfer Object) : 資料傳輸物件,業務層向外傳輸的物件。如果在某個業務中需要多次查詢獲取不同的資料物件,最後將會把這多個數據物件組合成一個 DTO 並對外傳輸。所在包命名為 dto 。
- BO(Business Object) : 業務物件,由 Service 層輸出的封裝業務邏輯的物件。即物件除了資料外,還會包含一定的業務邏輯,也可以說是充血領域模型。但是我一般不會使用。
- AO(Application Object) : 應用物件,在 Web 層與 Service 層之間抽象的複用物件模型,極為貼近展示層,複用度不高。比較少用。
- VO(View Object) : 顯示層物件,通常是 Web 向模板渲染引擎層傳輸的物件。現在的專案多數為前後端分離,後端只需要返回 JSON ,那麼可以理解為 JSON 即是需要渲染成的“模板”。我一般會將這類物件命名為 xxxResponse ,所在包命名為 response 。
- Query : 資料查詢物件,資料查詢物件,各層接收上層的查詢請求。其實一般用於 Controller 接受傳過來的引數,可以將其都命名為 xxxQuery ,而我個人習慣將放在 request body 的引數(即 @RequestBody)包裝為 xxxRequest ,而如果使用表單傳輸過來的引數(即 @RequestParam)包裝為 xxxForm ,並分別放在包 request 和包 form 下。
其實貧血領域模型只是作為資料的載體,在一開始我覺得沒必要進行具體的分類,基本上都是往一個包內丟,但是當專案規模上來後,各種各樣的資料實體開始增加,慢慢的變得混亂。對資料物件的分類是為了更好的定義每個資料的作用以及在後續能夠快速的定位到對應的資料物件。
2.4、Helper
開發中會遇到一些很基礎的,通用的業務邏輯,例如我們可能會根據每個使用者的資訊生成一個唯一的 account id 。又或者說有一個使用者排名的需求,我們將從使用者的相關資訊中計算出一個分數,從而根據這個分數進行排名。那麼這時候我們可能會將這些邏輯寫在 User 資料物件或是其他相應對應的資料物件下。
而我個人來說,不希望資料物件包含業務邏輯,所以我會將這些通用的業務邏輯都抽出來,放到 Manager 中進行統一管理。如會將生成 account id 的邏輯放在 AccountIdGenerator 中,將計算排名分數的邏輯放在 RankCalculator 中。
我將這些類都歸為 Helper ,用於提供底層的業務計算邏輯。而為什麼不放在通用工具層中呢?因為這些 Helper 其實都是依賴於特定的領域,即特定的業務。而通用工具類則是業務無關的,任何系統,只要有需要都可以引用。
2.5、DAO
DAO 就不用過多解釋了,資料訪問物件,用於對資料庫的訪問。但是我個人不會將 DAO 只侷限於資料庫,對於不同的資料來源的互動,如 HBase ,ElasticSearch ,本地快取甚至 Redis 我都會定義相對應的 DAO 進行訪問。
這樣的定義,其實是想將資料 CURD 的邏輯和業務邏輯進行分離,將獲 CRUD 封裝在 DAO 中,業務邏輯即放在業務層中。
之前接手了一個專案,專案將 Redis 視為中介軟體,將相關的邏輯都封裝在 xxxRedisService 中,包括 CRUD 和一些業務邏輯。隨著專案的發展,一些其實可以歸類到一起的業務,變得有些放在了 RedisService 中,一些放在了業務層的 Service 中,可想而知十分混亂,還導致了一些 BUG 的出現。
2.6、Service 和 Manager
Service 的作用不用多說明,為具體業務邏輯的封裝層。
具體要說明的是 Manager ,《阿里巴巴 Java 開發手冊》中定義如下:
- 對第三方平臺封裝的層,預處理返回結果及轉化異常資訊
- 對 Service 層通用能力的下沉,如快取方案、中介軟體通用處理
- 與 DAO 層互動,對多個 DAO 的組合複用
可以將 Manager 理解為對通用邏輯的封裝,避免 Service 與 Service 進行相互呼叫,以及對通用邏輯的管理。
在開發中,我們經常會遇到 AService 中的某個業務可以提供給 BService 呼叫,從而讓 BService 呼叫 AService 的方法,認為是 Service 之間具有共同的業務。其實 Service 之間沒有共同的業務,而是具備通用的邏輯,這時應該將其抽離出來放在 Manager 中。無論何種工程架構都好,我都不贊同 Service 與 Service 之間的相互呼叫。
在實際開發中,我會對 Manager 進行更細一點的劃分。大致將其分為用於項務類,所封裝的是由 Service 下沉的通用業務。
而另一種則是一些偏向於工具、計算的類,例如某個業務使用了策略模式,所編寫的策略類則屬於這一類。
我會將業務類的用 @Service 註釋,而偏工具類的則用 @Component 註釋。這樣做的原因還是避免業務之間的相互呼叫,相互耦合。
這裡可能會想,為什麼不將 Helper 的邏輯也放在 Manager 層中?原因在於 Helper 的邏輯比 Manager 更加基礎,有可能在 DAO 中都會呼叫 Helper 的相關邏輯,如果放在 Manager 中,就會出現底層依賴上層的問題。
2.7、介面層
最後的一層,則是暴露給外部呼叫的層。可以是 Spring 體系中的 Controller ,也可以是 gRPC 。
這一層將組織、呼叫我們所定義的 Service ,進行業務處理。
3、分層模型的優點以及缺點
無論什麼工程架構,都會有其優點以及缺點,在選擇工程架構時,其實就是對優點缺點的衡量。
3.1、優點
其實無論什麼架構,特別是對業務工程來說,最希望架構帶來的是解耦以及內聚。
通過分層,在一定程度上對專案內的各個模組進行了解耦內聚,依賴關係十分明確,再怎麼寫,只要符合規約,總是上層依賴於下層。而且分層的規約十分簡單,在多人協作的情況下大部分情況都可以很好的遵守規約。
3.2、缺點
簡單是一個優點,也是一個缺點。分層雖然在一定程度上進行了解耦,但是粒度十分粗,只要不出現下層依賴上層的情況,都可以認為是符合規約的,在這種情況下,很容易導致程式碼的分散、功能的碎片化,明明是同一類業務功能的程式碼,卻分散在多個類,多個層次之間。在專案不斷迭代時變得巨大時,慢慢就會變得混亂,然後就是一輪重構。
歸根到底就是太鬆懈了,導致開發人員很容易就是在專案中隨便找個地方寫,還很容易導致由大量的複製貼上所產生重複程式碼。
在學校開設的軟體工程課中,設計一個系統,首先是組織架構的瞭解,然後從中抽出資料流,然後再在資料流中抽出業務流,進行根據業務流進行開發。而採用分層模型的化,往往在資料流中就可以開始開發,採用分層模型的話,每個業務其實可以簡單的抽象成資料在各層之間的流動。
這可以說是一個優點,簡化了業務的理解,實現快速的開發,我在比較緊的排期下也由這麼做過,掃一眼業務,構思好資料流的流動後就動手了。但這也是一個很嚴重的缺點,我見過不少功能性 BUG ,就是由於對業務的不充分理解所導致的,而且由於沒有對業務流程充分理解後就開發,後續的擴充套件和修復,看起來就是不斷的修修補補。
上面,我除了《阿里巴巴 Java 開發手冊》所寫的內容外,還添加了不少細節,其實所想要做的就是儘量減少這種功能碎片化的問題。
4、與充血領域模型的對比
既然是說工程架構,就不得不提 DDD 這一個概念。
為什麼我說的是“與充血領域模型的對比”而不是“與 DDD 的對比”呢?是因為 DDD 是比分層模型更加高層的一種概念,它是一個產品服務,整個團隊開發的一種指導思想,而不是一種工程程式碼上的規約。
DDD 可以分為兩大方向,一個是戰略層面上的,即之前提到的是一種開發的指導思想,定義、劃分服務的領域,規定統一語言提高溝通效率等,這也是可以用於使用分層領域模型的專案開發中的。如果要與分層模型對比的話,其實是 DDD 的戰術層面,即充血領域模型。
充血領域模型其實是迴歸於面向物件的思想。在目前的分層模型中,哪怕是用 Java 這種面向物件的語言去寫,其實總體上還是一種過程式的程式設計,在 DDD 中稱為事務指令碼。
充血領域模型是重領域,輕 Service 的。以之前生成 account id 以及排名的例子,在充血領域模型中,User 類將會有 generateAccountId 方法和 ranking 方法來完成這一邏輯。
完全的面向物件,就可以充分的發揮面向物件的特性。面向物件的特性在書上為:繼承、多型,封裝。前兩者能夠實現歸一化,使模組泛化通用,封裝即會使模組劃分明確,能夠很好的實現解耦和內聚。比起分層模型,使用充血領域模型可以很好的解決上面提到的程式碼分散,碎片化的問題。
充血領域模型的優點是面向物件的優點,但是面向物件的缺點也成為這種模型的缺點。首先,萬物皆可抽象在我看來就是偽命題,因為現實世界中總有事務是難以進行抽象的,或者抽象起來不優雅,總是有一種硬是抽象的感覺。
在知乎中有一個很好的回答,描述了面向物件的弊端
相信很多人在初接觸 DDD 時,都會去搜索充血領域模型實踐的例子。其實在學校學習 Java Web 開發時,書本中寫道的 MVC 結構其實在一定程度上也是充血領域模型,Model 除了是資料的載體外,還包含業務邏輯,通過 Controller 對 Model 的選擇以及呼叫完成業務。假如用這種結構開發,當專案龐大後,我覺得首先遇到的問題應該就是依賴問題,複雜的業務必然牽扯到各方各面,自然也就有複雜的依賴關係產生,甚至會有為了完成業務而產生很“髒”的實現,這是難以避免的。
我個人覺得充血領域模型目前還是隻適合於個人,很小的團隊中使用,例如 2 到 3 個人的團隊,因為抽象本身就是一個非常複雜的過程,隨著需求迭代,之前的抽象還不一定正確,如果在較為多人的多人協作中,各種奇奇怪怪的寫法都會出現,必然也會有隨便找個“地”寫的情況出現,這種情況比在分層模型中更為致命。
5、總結
還是那句話,工程架構無分好壞,只有適合與不適合,問題的來與在於業務的複雜,計算機始終在某些方面難以對映到現實世界。所以我個人建議好好的理解好自己目前所用到的工程架構,儘量做到揚長避短。
歡迎關注微信公眾號”程式設計師小明”,獲取更多資源。