簡潔架構設計:如何設計一個合理的軟件架構?
提示:
-
本文摘自: 「雲原生 AI 實戰營」 知識星球「Go 項目開發中級實戰課」的第 04 | 簡潔架構設計:如何設計一個合理的軟件架構?[1] 節課。
-
本文介紹的實戰項目 GitHub 地址爲:https://github.com/onexstack/miniblog
在開發項目之前,需要先設計一個合理的軟件架構。一個好的軟件架構不僅可以大大提高項目的迭代速度,還可以降低項目的閱讀和維護難度。目前,行業中有多種流行的軟件架構,例如:MVC 架構、六邊形架構、洋蔥架構、簡潔架構等。在 Go 項目開發中,用的最多的是簡潔架構。
本節課會詳細介紹簡潔架構,以及 miniblog 項目的簡潔架構設計和實現方法。
爲什麼需要軟件架構?
這裏引用 Robert C.Martin 在其《Clean Architecture》書中的一句話,來說明爲什麼需要軟件架構:軟件架構的目標是最大限度地減少構建和維護系統所需的人力資源。
具體而言,採用一個合理的軟件架構將帶來以下好處:
-
可測試性:良好的軟件架構能夠提高代碼的可測性,從而增強軟件的穩定性;
-
可維護性:良好的軟件架構使系統的各個部分相互獨立,易於理解和修改。它提供了結構化的方式來組織代碼,使系統的修改和維護變得更加簡單;
-
擴展性:軟件架構應能夠很好的支持系統的擴展和演變。通過合理的分層和模塊化,軟件架構可以使系統的功能很容易的得到擴展,而無需對整個系統進行重構;
-
可重用性:好的軟件架構能夠提高代碼的複用度。將通用的功能封裝爲可複用的包 / 庫,可以使這些功能在不同的項目和模塊中重複使用,從而提高開發效率和代碼質量。
簡潔架構介紹
簡潔架構(Clean Architecture)是一種軟件架構模式(又稱整潔架構、乾淨架構),旨在實現可維護、可測試和可擴展的應用程序。最初由 Robert C.Martin 在其文節課 The Clean Architecture 提出。之後,因爲簡潔架構的諸多優點,在 Go 項目開發中被大量採用。
軟件架構有多種形式,例如六邊形架構、洋蔥架構、尖叫架構、DCI 架構和 BCE 架構等。這些架構在細節上各有不同,但整體而言非常相似。它們的共同目標是實現關注點的分離,並通過軟件的分層設計來達到這一目的,從而踐行高內聚、低耦合的架構理念。
採用這些軟件架構開發的應用都具有以下五點特性:
-
**獨立於框架:**該架構不會依賴於某些功能強大的軟件庫存在。這可以讓開發者使用這樣的框架作爲工具,而不是讓開發者的系統陷入到框架的約束中;
-
**可測試性:**業務規則可以在沒有 UI、數據庫、Web 服務或其他外部元素的情況下進行測試,在實際的開發中,可以通過 Mock 來解耦這些依賴;
-
**獨立於 UI:**在無需改變系統其他部分的情況下,UI 可以輕鬆地改變。例如,在沒有改變業務規則的情況下,Web UI 可以替換爲控制檯 UI;
-
**獨立於數據庫:**開發者可以用 Mongo、Oracle、Etcd 或者其他數據庫來替換 MariaDB,開發者的業務規則不要綁定到數據庫;
-
**獨立於外部媒介:**實際上,開發者的業務規則可以簡單到根本不去了解外部世界。
上述五點特性,也可以看作是簡潔架構的五點約束,理論上任何遵循了以上五點約束的軟件架構,都可以看作是簡潔架構的一種實現方式。通常所說的簡潔架構指的是洋蔥架構。
提示: Robert C. Martin 還爲簡潔架構專門寫了一本書,如果你想了解更多簡潔架構的知識,可閱讀圖書《架構整潔之道》。
miniblog 簡潔架構實現
任何實現簡潔架構規定的五個約束的軟件架構均可稱爲簡潔架構。miniblog 項目參考業界簡潔架構的實現,也設計實現了一種簡潔架構。與其他簡潔架構的最大區別在於,miniblog 的簡潔架構設計更加簡單實用,省略了一部分分層特性,僅保留了必要的分層,但帶來了更大的易讀性和可維護性。
miniblog 項目的簡潔架構設計如下圖所示。
整個軟件架構一共分爲以下三層:
-
**Handler 層:**負責 API 接口請求的參數解析、參數校驗、業務邏輯處理分發、參數返回邏輯。在 Handler 層中,還有 Default 和 Validation 模塊,分別用來給請求參數設置默認值,並校驗請求參數的合法性;
-
**Biz 層:**包括了具體的業務邏輯實現。Biz 層根據 REST 資源類型分爲不同的模塊,內部可模塊間交叉調用。在 Biz 層還有 Conversion 模塊,用來進行結構體類型轉換;
-
**Store 層:**數據訪問層(包括訪問數據庫或第三方微服務),用來跟數據庫 / 微服務交互執行數據的 CURD 操作。該層做了進一步的抽象,抽象出了通用的 Store 層,Generic Store 之上 REST 資源的數據存儲操作,均可繼承 Generic Store 的方法實現,而不需要自行再實現一套。
上圖所示的簡潔架構,還具有以下特點:
-
簡潔架構提供了清晰的分層結構,各層功能明確,職責分明;
-
通過接口解耦每一層,從而實現代碼的可測性、獨立性和擴展性;
-
代碼依賴由上向下(圖中的有向箭頭表示依賴規則),單向單層依賴,提供了清晰的依賴關係,使代碼易於理解和維護。
上述三個特點也使得整個軟件代碼具有很高的易讀性和可維護性。圖 3-1 所示的簡潔架構有三層,但這不意味着簡潔架構只有三層。如果有需要你可以對層進行增減。雖然層數可變,但是依賴關係是固定的,即:單向依賴。
上圖所示的簡潔架構中,API 請求的數據流轉路徑如下圖所示。
請求到來後,先經過 Default 模塊,用來給請求參數設置默認值。之後,經過 Validation 模塊,用來對請求參數進行校驗。校驗通過後,會經過 Handler 方法,Handler 方法會處理請求,並將請求轉發到 Biz 層的 Biz 方法中。在 Biz 方法中需要進行數據轉換,在 miniblog 項目中,會將 Biz 層的數據結構轉換爲 Store 層的數據結構,並調用 Store 層的方法,對數據進行 CURD 操作。Store 層的方法繼承自 Generic Store,所以最終是調用 Generic Store 完成對數據的 CURD 操作。
簡潔架構中的依賴規則
簡潔架構能夠工作的關鍵是依賴規則,這條規則規定:代碼依賴應該由上向下,單向依賴。這種依賴包含代碼名稱、函數 / 方法、變量或任何其他軟件實體。也就是說,下層不應該感知到上層的任何對象。上層中聲明的數據格式不應被下層使用。
除了上述層與層之間的包依賴關係外,各層還可以導入項目所需的其他 Go 包,例如內部包、外部包或框架包等。然而,必須確保依賴關係的合理性,避免出現循環依賴。
上述包導入關係如下圖所示。
簡潔架構中的分層設計
miniblog 的簡潔架構一共分爲了三層:存儲層(Store)、業務層(Biz)、處理器層(Handler)。每一層都承載了不同的功能。
存儲層(Store)
存儲層在某些簡潔架構設計中也稱爲 Frameworks&Drivers 層或基礎設施層。存儲層負責與數據庫、外部服務等進行交互,作爲應用程序的數據引擎進行數據的輸入和輸出。需要注意的是,存儲層僅對數據庫或外部服務執行 CRUD 操作,不封裝任何業務邏輯。
此外,存儲層還承擔數據轉換的任務:將從數據庫或微服務獲取的數據轉換爲處理器層和業務層能夠識別的數據結構,同時將處理器層和業務層的數據格式轉換爲數據庫或外部服務可識別的數據格式。
業務層(Biz)
業務層在(Biz,Business)某些簡潔架構設計中也稱爲 Usecases 層。業務層是領域模型的應用層,負責協調各個實體和值對象之間的交互,以完成具體的業務需求。業務層會受到業務邏輯變更的影響,但不會被其他層所影響,例如用戶界面和數據庫等。
業務層功能如下圖所示。
在實際的企業應用開發中,業務層是變更最頻繁的一層。
處理器層(Handler)
處理器層在某些簡潔架構設計中也稱爲控制器層。處理器層負責接收 HTTP/RPC 請求,並進行參數解析、參數校驗、業務邏輯處理、請求返回等操作。處理器層的核心目的是將用戶的輸入轉化爲領域模型的操作,並將結果返回給用戶。在這一層還包括其他適配器,用於將數據從外部形式(如外部服務)轉換爲業務層可以使用的內部形式。
處理器層會將請求轉發給業務層,業務層處理後返回,返回數據在處理器層中被整合再加工,最終返回給請求方。處理器層相當於實現了業務路由的功能。具體流程如下圖所示。
提示:
在 MVC 架構中,處理器層通常用 Controller 來表示,而在 gRPC 服務中則用 Service。爲了統一 MVC 架構中的處理器層名稱與 gRPC 服務中的處理器層名稱,這裏統一使用 Handler 來表示處理器層。在大多數 Go 項目中,包括一些優秀的開源項目(如 Kubernetes、Gin、Echo 等),處理請求的層通常被命名爲 Handler,而非 Controller 或其他名稱。Handler 準確地表達了其職責,即負責處理(handling)請求。
接口依賴關係
在簡潔架構的設計中,各層之間通過接口進行解耦,以便減少依賴關係,同時增強系統的擴展性。接口依賴有以下兩種模式:
-
**接口依賴方式一:**外層組件聲明所需的能力,內層組件則實現這些能力;
-
**接口依賴方式二:**內層組件首先提供所需能力(接口),外層組件才能調用這些能力。外層組件的能力依賴於內層組件的能力。
接口依賴模式如下圖所示。
在接口依賴方式一種,包的導入關係爲內層導入外層。在接口依賴方式二中,包的導入關係爲外層導入內層。miniblog 項目採用了第二種接口依賴方式,即在開發過程中優先開發內層組件,然後再開發外層組件。具體的開發流程爲:先開發 Store 層、Biz 層,最後是 Handler 層。
層之間的通信
處理器層、業務層和存儲層之間均通過接口進行通信。通過接口通信,一方面可以支持同一個功能有不同的實現(也就是說具有插件化能力)。另一方面,接口解耦了不同層的具體實現,使得每一層變得獨立且可測試。層之間通信模式如下圖所示。
簡潔架構如何測試
處理器層、業務層和存儲層之間通過接口進行通信。通過接口通信的一個好處是,可以讓各層變得可測試。本節將討論如何測試各層的代碼。
存儲層測試
存儲層依賴於數據庫,如果調用了其他微服務,則還會依賴第三方服務。開發者可以通過 sqlmock 來模擬數據庫連接,通過 httpmock 來模擬 HTTP 請求。
業務層測試
業務層依賴於存儲層,這意味着該層需要存儲層的支持才能進行測試。可以使用 golang/mock 來模擬存儲層,測試用例可以參考 Test_postBiz_Delete,單元測試用例代碼如下述代碼所示。
func Test_postBiz_Delete(t *testing.T) {
// 創建一個新的 gomock 控制器,用於管理 Mock 對象
ctrl := gomock.NewController(t)
defer ctrl.Finish() // 確保在測試結束時調用 Finish,以驗證所有預期的調用
// 構造 Mock 的 PostStore
mockPostStore := store.NewMockPostStore(ctrl)
// 設置對 Delete 方法的期望:在上下文中調用一次,傳入任意參數,返回 nil(表示沒有錯誤)
mockPostStore.EXPECT().Delete(context.Background(), gomock.Any()).Return(nil).Times(1)
// 構造 Mock 的 IStore
mockStore := store.NewMockIStore(ctrl)
// 設置對 Posts 方法的期望:可以被調用任意次數,返回 mockPostStore
mockStore.EXPECT().Post().AnyTimes().Return(mockPostStore)
// 初始化 postBiz 實例,傳入 Mock 的 IStore
biz := &postBiz{store: mockStore}
// 執行 Delete 方法,傳入上下文和一個空的 DeletePostRequest
got, err := biz.Delete(context.Background(), &apiv1.DeletePostRequest{})
// 使用 assert 進行斷言,檢查返回的結果是否與期望的 DeletePostResponse 相等
assert.Equal(t, &apiv1.DeletePostResponse{}, got, "Expected response does not match")
// 檢查 err 是否爲 nil,確保沒有錯誤發生
assert.Nil(t, err, "Expected no error, but got one")
}
上述代碼使用 golang/mock 工具生成了存儲層的 Mock 方法 NewMockPostStore 和 NewMockIStore。
處理器層測試
處理器層依賴於業務層,這意味着該層需要業務層的支持進行測試。同樣可以通過 golang/mock 來模擬業務層,測試用例可參考 TestHandler_DeletePost。
小結
本節課介紹了 miniblog 項目中使用的簡潔架構設計。在 miniblog 項目中,架構被簡化爲存儲層、業務層和處理器層,各層通過接口解耦,職責明確,提升了代碼的清晰度和可測試性。遵循的依賴規則確保了代碼依賴由外向內、單向傳遞。此外,接口的使用提升了層之間的通信能力,使得各層可以獨立測試,使得可以通過單元測試用例來提高代碼的穩定性。
參考資料
[1]
04 | 簡潔架構設計:如何設計一個合理的軟件架構?: https://t.zsxq.com/kwzqd
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/sYT4IhDoklGWZQ9btSB-qw