簡潔架構設計:如何設計一個合理的軟件架構?

提示:

在開發項目之前,需要先設計一個合理的軟件架構。一個好的軟件架構不僅可以大大提高項目的迭代速度,還可以降低項目的閱讀和維護難度。目前,行業中有多種流行的軟件架構,例如:MVC 架構、六邊形架構、洋蔥架構、簡潔架構等。在 Go 項目開發中,用的最多的是簡潔架構。

本節課會詳細介紹簡潔架構,以及 miniblog 項目的簡潔架構設計和實現方法。

爲什麼需要軟件架構?

這裏引用 Robert C.Martin 在其《Clean Architecture》書中的一句話,來說明爲什麼需要軟件架構:軟件架構的目標是最大限度地減少構建和維護系統所需的人力資源。

具體而言,採用一個合理的軟件架構將帶來以下好處:

簡潔架構介紹

簡潔架構(Clean Architecture)是一種軟件架構模式(又稱整潔架構、乾淨架構),旨在實現可維護、可測試和可擴展的應用程序。最初由 Robert C.Martin 在其文節課 The Clean Architecture 提出。之後,因爲簡潔架構的諸多優點,在 Go 項目開發中被大量採用。

軟件架構有多種形式,例如六邊形架構、洋蔥架構、尖叫架構、DCI 架構和 BCE 架構等。這些架構在細節上各有不同,但整體而言非常相似。它們的共同目標是實現關注點的分離,並通過軟件的分層設計來達到這一目的,從而踐行高內聚、低耦合的架構理念。

採用這些軟件架構開發的應用都具有以下五點特性:

上述五點特性,也可以看作是簡潔架構的五點約束,理論上任何遵循了以上五點約束的軟件架構,都可以看作是簡潔架構的一種實現方式。通常所說的簡潔架構指的是洋蔥架構。

提示: Robert C. Martin 還爲簡潔架構專門寫了一本書,如果你想了解更多簡潔架構的知識,可閱讀圖書《架構整潔之道》。

miniblog 簡潔架構實現

任何實現簡潔架構規定的五個約束的軟件架構均可稱爲簡潔架構。miniblog 項目參考業界簡潔架構的實現,也設計實現了一種簡潔架構。與其他簡潔架構的最大區別在於,miniblog 的簡潔架構設計更加簡單實用,省略了一部分分層特性,僅保留了必要的分層,但帶來了更大的易讀性和可維護性。

miniblog 項目的簡潔架構設計如下圖所示。

整個軟件架構一共分爲以下三層:

上圖所示的簡潔架構,還具有以下特點:

上述三個特點也使得整個軟件代碼具有很高的易讀性和可維護性。圖 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