探討篇(二):分層架構的藝術 - 打造合理且高效的架構體系

上篇從服務粒度角度進行了探討,本文繼續從服務內的分層角度探討。 本文的觀點源自我在學習與實踐過程中的深思熟慮,尚處於不斷探索和驗證的階段。希望能“拋磚引玉”,激發更多的討論與交流。讓我們共同進步,在探討與實證中尋求真知。

一、背景

應用分層看似直觀,但實踐中常見誤區:開放接口Api層(或controller層)邏輯繁複,manager層調用混亂,service層淪爲傳輸站。這種疏忽導致代碼重用性差,層次混亂,維護成本增加。其實在許多系統中,普遍都存在着層次邏輯不明確、模塊間循環依賴以及代碼擴展性差、修改A功能影響B邏輯牽一髮動全身等問題,這些都會引發連鎖反應,進而影響系統的整體穩定性。



先來3個很常見的案例,看看代碼是否合理?如果合理/不合理?背後原因是什麼?

案例1:Controller層調用Dao層

public class AController {
   @Autowired
   private ADao aDao;
}

案例2:A服務的AService層調用B服務的BDao層 合理嗎?

public class AServiceImpl {
   @Autowired
   private ADao aDao;
   @Autowired
   private BDao bDao;
}

案例3:A服務的AService層調用manager層,manager層反過來調用service層,是否合理?

public class AService {
   @Autowired
   private AManager amanager;
}

public class AManager {
   @Autowired
   private BService bservice;// Manager層依賴了Service層
}

二、分層拓撲結構

分層思想是應用系統最常見的一種架構模式,它是將整體系統拆分成 N 個層次,每個層次有獨立的職責,多個層次協同提供完整的功能。我記得作爲程序員的時候,第一個接觸系統的設計就是「MVC」架構。它將整體的系統分成了 Model(模型),View(視圖)和 Controller(控制器)三個層次,也就是將用戶視圖和業務處理隔離開,並且通過控制器連接起來,很好地實現了表現和邏輯的解耦,是一種標準的軟件分層架構。



大多數分層結構是有四個標準層構成:展示層、業務層、持久層、數據庫層。





上圖從物理分層(部署)的角度說明各種拓撲結構的變體。比如第三種變體對於具有內部嵌入式數據庫或者內存數據庫的小型應用程序可能很有價值。很多外包預製的產品都採用第三種變體構建給客戶。



分層架構中每一層在架構中都有特定的角色和職責。比如表現層負責用戶界面和瀏覽器邏輯處理,而業務層負責執行與請求關聯的特定業務規則。



分層架構是一種技術劃分的架構,和領域分區的架構相反。比如promise產能域包含在每一層。

三、分層隔離性

先講幾個概念

1.開放層:開放層說明這層對上面請求都開放,可以跳過任何層訪問該層。比如controller層可以直接訪問dao層。

2.封閉層:封閉層跟開放層相反。封閉意味着一個請求從頂向下從一個層到另外一個層,請求不能跳過任何層。必須通過下面的層一層一層到達下面的層。 比如service層是封閉的。那麼controller層不能直接訪問dao層,必須經過service層,service層訪問dao層。





如上圖請求必須沿着一定的方向逐層傳遞,不能跨層——每一層都是 closed 的,這樣做的好處是隔離了變化,某一層的變化隻影響相鄰層,而不會影響其他層。 特殊情況下,某層也可以設置爲 open —— 即允許被跨越。

分層架構中的每一層既可以封閉也可以開放。要回答上面3個案例這個代碼分層是否合理(要求業務層和持久層是開放的)?答案關鍵在於分層隔離性。



分層隔離性意味着在架構的一個分層中所做的更改通常不會影響其他分層的組件,前提是這些層之間的契約保持不變。由於每個層都有自己的職責。並且對上層而言是透明的。只有將職責限制在自己的邊界內,整體層次結構才清晰明瞭。

1.爲了支持分層隔離性。與主請求相關的層必須是封閉的。如果展示層可以直接訪問持久層,那麼對持久層做的更改會影響業務層和展示層。從而產生程序組件相互依賴,架構變的脆弱,更改困難。

2.分層隔離性還允許在不影響任何其他層的情況下替換架構中的任何一層。

3.封閉層促進了分層隔離性。有助於更改,但有時候開放某些層是有意義的。

四、分層架構本質

那麼分層架構的本質是什麼呢?其實我們仔細思考,你會發現不管是跨進程的分層架構,還是進程內的MVC分層,都是一個“數據移動”,然後“被處理”和“被呈現”的過程,歸根結底一句話:互聯網分層架構,是一個數據移動,處理,呈現的過程。

數據是移動的:

•跨進程移動:數據從數據庫和緩存裏,轉移到service層,到controller層,到瀏覽器client層

•同進程移動:數據從model層,轉移到control層,轉移到view層

數據要移動,有兩個東西很重要:

數據傳輸的格式

數據在各層次的形態

先看數據傳輸的格式,即協議很重要:

•service與db/cache之間,二進制協議/文本協議是數據傳輸的載體

•server與service之間,RPC的二進制協議是數據傳輸的載體

•client和web-server之間,http協議是數據傳輸的載體

再看數據在各層次的形態,以用戶數據爲例:

•db層,數據是以“行”爲單位存在的row(uid, name, age)

•cache層,數據是以kv的形式存在的kv(uid -> User)

•service層,會把row或者kv轉化爲對程序友好的User對象

•web-server層,會把對程序友好的User對象轉化爲對http友好的json對象

•client層:最終端上拿到的是json對象



五、分層架構演進的核心原則與方法

那麼當我們要做分層設計的時候,需要考慮哪些關鍵因素呢?

1.是否需要增加一層?

2.是否需要服務化?

3.是否需要抽取通用業務?

這些問題,其實不好回答。最主要的一點就是你需要理清楚每個層次的邊界是什麼當業務邏輯簡單時,層次之間的邊界的確清晰,開發新的功能時也知道哪些代碼要往哪兒寫。但是當業務邏輯變得越來越複雜時,邊界就會變得越來越模糊。這將會引出“分層架構演進”的核心原則與方法:

讓上游更高效的獲取與處理數據,複用

讓下游能屏蔽數據的獲取細節,封裝

弄清楚這個原則與方法,再加上一些經驗積累,就能回答提出的這些問題了:



那如何讓數據的獲取更加高效快捷呢?通用業務服務層的抽象勢在必行。







開放接口層(JSF接口): 比如對外提供的JSF服務接口,可將 Service 層方法封裝成RPC開放接口。通過JSF進行限流熔斷等控制。這是JSF服務的第一層,類似controller層。這一層代碼核心是輕業務邏輯、入參校驗、異常兜底。

業務邏輯層(Service層):複用性低,核心是****業務編排邏輯。

通用處理層(Manager層)可複用邏輯。 這一層主要有如下作用:

1.可以將原先 Service 層的一些通用能力下沉到這一層,比如與緩存和存儲交互策略,中間件MQ等接入;

2.你也可以在這一層封裝對公司第三方RPC或者外部接口的調用,比如調用GIS、訂單中間件服務等。

3.跟DAO層交互,對多個DAO的組合複用。

Manager 層與 Service 層的關係是:Manager 層提供原子的服務接口,Service 層負責依據業務邏輯來編排原子接口。



再來回答上面代碼問題。

案例1:不推薦Controller層直接訪問DAO層。原因如下:

1.破壞分層架構:破壞了清晰的分層架構,降低了代碼的可讀性和可擴展性。

2.增加系統脆弱性:沒有經過Service層的業務邏輯校驗直接操作數據庫,可能會引起數據不一致或者安全隱患。

3.代碼維護性差:對於簡單的數據操作,雖然看似直接從Controller調用DAO能減少代碼量,但長遠來看,這種做法會使得系統難以維護和擴展。



案例2:A服務的Service層應該調用B服務的Service層而不是直接調用其DAO層。原因如下:

1.職責劃分清晰:AService層應當通過BService層來進行通信,而不是直接調用B底層的DAO層。這樣做可以保持清晰的層次結構,每一層的職責明確,便於維護和擴展。

2.業務邏輯隔離:B服務的Service層封裝了對DAO的操作和業務邏輯,比如對Cache緩存的處理。如果A服務直接調用B服務的DAO,將無法利用B服務的業務邏輯處理能力,可能導致業務邏輯錯誤或不一致。

3.封裝性:如果B服務的DAO層直接被A服務使用,那麼B服務的內部的實現細節將會暴露給A服務,這違反了封裝原則。正確的做法應該是B服務提供Service層接口供A服務調用,這樣A服務不需要關心B服務的實現細節。

案例3:A服務的AService層調用manager層,manager層不能反過來調用service層。原因如下:

1.Manager層再回過頭來調用Service層通常是不合理的,因爲這破壞了層次結構的單向性和各層的職責邊界。其他原因跟案例2類似

六、分層模型規約

1、領域模型

•DO(Data Object):此對象與數據庫表結構一一對應,通過DAO層向上傳輸數據源對象。

CO(Cache Object):由於promise時效業務數據強依賴緩存數據,爲了跟數據庫表結構區分,特定義了一個CO,此對象與Redis中存儲的結構一一對應。通過CAO層向上傳輸數據源對象。

•DTO(Data Transfer Object):數據傳輸對象,Service或Manager向外傳輸的對象。

•BO(Business Object):業務對象,可以由Service層輸出的封裝業務邏輯的對象。

•VO(View Object):顯示層對象,通常是Web向模板渲染引擎層傳輸的對象。

2、分層異常處理模型

1.在DAO層,由於可能會遇到多種類型的異常,建議使用catch(Exception e)的方式進行捕獲,並拋出一個自定義的DAOException,DAO層不需要打印日誌。這是因爲在Manager或Service層,異常會被再次捕獲並記錄到日誌文件中。

2.在 Service 層出現異常時,必須記錄出錯日誌到磁盤,其中日誌記錄應該遵循一定的規範,包括錯誤碼、異常信息和必要的上下文信息。日誌內容應該清晰明瞭,相當於保護案發現場。

3.異常封裝:對於業務層面的異常,應當進行適當的封裝,定義統一的異常模型。避免直接將底層異常暴露給上層模塊,以保持業務邏輯的清晰性。比如DependencyFailureException:表示服務端依賴的其他服務出現錯誤,服務端是不可用的,可以嘗試重試,類比HTTP的5XX響應狀態碼。InternalFailureException:表示服務端自身出現錯誤,服務端是不可用的,可以嘗試重試,類比HTTP的5XX響應狀態碼。

4.Web 層絕不應該繼續往上拋異常,因爲已經處於頂層,無繼續處理異常的方式,如果意識到這個異常將導致頁面無法正常渲染,那麼就應該直接跳轉到友好錯誤頁面,加上友好的錯誤提示信息。

5.開放接口層不能直接拋異常,應該將異常處理成code錯誤碼和錯誤信息message方式返回。其中錯誤碼應該能夠快速識別錯誤的來源,便於團隊成員快速定位問題。同時,錯誤碼應易於比對,有助於團隊對錯誤原因達成共識。其中錯誤編碼可參考HTTP協議的響應狀態碼:

•2XX(成功響應):表示操作被成功接收並處理。例如,200表示請求成功。

•4XX(客戶端錯誤):表示請求包含語法錯誤或無法完成請求。例如,404表示請求的資源(網頁等)不存在。

•5XX(服務端錯誤):表示服務器在處理請求的過程中發生了錯誤。例如,500表示服務器內部錯誤,無法完成請求。

七、探討點

1.分層架構需要注意架構污水池反模式。什麼意思呢?當請求作爲簡單的傳遞一層到另外一層。而在每一層不做業務邏輯時,就會出現這種反模式。那如何處理呢?根據2/8法則,如果20%請求是這樣可以接受,反過來如果80%代碼請求都是這樣,則說明當前分層架構不適合,這時候應該開放架構中的所有層

2.分層架構的思想旨在通過橫向切分和根據業務職責的劃分來規劃軟件系統的邏輯結構,以便於開發和維護。然而,實際應用中,由於歷史代碼原因、團隊成員間編碼習慣的差異、以及編碼過程中優先考慮業務發展等因素,常常導致分層不徹底或混亂。這種分層的混亂不僅是因爲開發者沿襲前人的代碼習慣,還因爲個人風格的差異,如在接口層混入業務邏輯,或在Service層頻繁調用遠程服務,進一步加劇了這一問題。當維護者面對與自己習慣迥異的代碼時,要在保持系統穩定性和按個人風格調整之間做出選擇,變得尤爲困難。 這種情況下,通過CodeReview已經爲時已晚或者很難發現,那如何在開發初期通過有效的策略,規避或儘量避免團隊破壞分層結構,成了一個迫切需要解決的問題。

八、總結

無論歷史代碼如何分層,只要新的分層能夠明確職責、簡化維護,並得到團隊的認同,那麼它就是有效的。

如果你有更好的分層思路,或者上面所描述的有什麼錯誤的地方還請留言指正一下。謝謝!




參考:

1.互聯網分層架構的本質: https://mp.weixin.qq.com/s/X1JnXFIkn57eyx3slKQKLQ

2.京東物流軟件系統穩定性建設方法

作者:京東物流 馮志文

來源:京東雲開發者社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章