清晰架構(Clean Architecture)的 Go 微服務: 設計原則

我最近寫了一個 Go 微服務應用程序,這個程序的設計來自三個靈感:

我使用 Spring 的基於接口的編程和依賴注入(Dependency Injection)來實現 Bob Martin 的清晰架構(Clean Architecture),並遵循了 Go 的簡單編程風格。當它們之間存在衝突時,進行了取捨。我只採用了 Clean Architecture 的設計原則(主要是 SOLID),因此實現的細節可能與其他 SOLID 實現不同。

我來自 Java 背景,對前兩個設計思想非常熟悉。在學習了 Go 之後,我逐漸認同了 Go 的簡單風格。粗略來說,有兩種不同的編程風格,一種是面向對象的, 它強調設計; 另一種是非面向對象的,它信奉用最簡單的代碼來實現用戶需要的功能,無需預先設計。 Go 更接近第二陣營,儘管它有一些面向對象的功能。 Go 的編程思路爲我提供了一個重新評估面向對象編程的新視角,並影響了我的編碼風格。結果是我只在必要時才進行面向對象的設計,而我更傾向於使用更簡單的解決方案而不是完美的方案。

設計原則:
  1. 基於接口編程(Programming on interface)

    本程序有三個主要業務層,用例(usecase),數據服務(dataservice)和域模型(model),其中只有域模型沒有接口,因爲沒有必要。 當你訪問外部服務時,你可以通過接口進行訪問。

    type sqlUserDataServiceFactory struct{}
        
    func (sudsf *sqlUserDataServiceFactory) Build(c container.Container, dataConfig *config.DataConfig)
        (dataservice.UserDataInterface, error) {
        
        dsc := dataConfig.DataStoreConfig
        dsi, err := datastorefactory.GetDataStoreFb(dsc.Code).Build(c, &dsc)
        if err != nil {
            return nil, errors.Wrap(err, "")
        }
        ds := dsi.(gdbc.SqlGdbc)
        uds := sqldb.UserDataSql{DB: ds}
        logger.Log.Debug("uds:", uds.DB)
        return &uds, nil
        
    }

    基於接口的編程的關鍵是將接口作爲參數傳遞給函數,並返回接口而不是具體類 型。 例如,在上面的代碼中,返回值 -“dataservice.UserDataInterface”,它是一個接口,而不是 struct。 調用函數不需要知道返回的具體結構,因爲接口封裝了它需要的所有信息。 這使你可以非常靈活地將返回的結構替換爲另一個結構,而不會影響調用函數。

  2. 用工廠方法模式(factory method pattern)通過依賴注入(Dependency Injection)創建具體類型.

    程序容器負責創建具體類型並將其注入函數。 我將在 “依賴注入(Dependency Injection)”⁸中進行詳細解釋.

  3. 建立正確的依賴關係

    它意味着以下內容:

    • 程序中的各層或組件都有自己的單獨的包。 接口在頂級包中定義,具體類型隱藏在子包中。
    • 不同層之間僅依賴於接口而不依賴於具體類型
    • 從頂層向下的依賴層次是:“用例”,“數據服務” 和 “模型”。

    衡量依賴關係質量的一種方法是看導入(import)語句的多少,導入語句越少,依賴關係越好。

  4. 開閉原則(Open-close principle)

    這是我最喜歡的設計原則。 它要求你在需要添加新功能時,不要修改現有代碼,而是添加新代碼。 實現它的方法是使用上面講到的#1 和#2。 這個原則有許多很好的現實世界的例子,例如,數據訪問對象(DAO)¹⁰。 好處是你不會無意中搞亂現有代碼,因爲只添加新代碼,這將大大減少測試工作量。

是否過度設計了?

與 Java 中的類似解決方案相比,由於 Go 的語言本身的簡單設計,本程序中的代碼量要少很多,也非常簡潔。 但是對於來自其他編程語言(特別是動態語言如 PHP,Ruby)的人來說,這個程序的設計可能有些重。 我也問了自己同樣的問題。 爲了得到答案,需要比較成本和收益以得出最終結論。

通常來說有兩種類型的需求變更,業務邏輯變更和技術方案變更。 在編寫業務代碼時,你不希望關注數據是來自 MongoDB 還是 MySQL 還是微服務。 在進行技術修改時,最大的噩夢是意外破壞業務邏輯。 一個好的設計將這兩種類型的編碼在程序中分開,讓你一次只關注一個。

一般來說,技術方案變更不會像業務邏輯變化那樣頻繁發生,但隨着微服務的普及,新技術將被更快地採用,這將加速技術變更。

設計帶來的好處:

以下是幾個示例,向你展示當需求變更時需要對程序進行的改動。 如果你看不太懂本節,可能需要先閱讀 “程序設計 ¹¹,它將爲你提供程序結構的描述。

從 MySQL 改成 MongoDB:

首先,假設我們需要將域模型 “User” 的持久層從 MySQL 更改爲 MongoDB。以下是步驟:

  1. 在 “appConfig [type] .yaml” 文件中添加 MongoDB 的新配置信息

  2. 將 “appConfig [type] .yaml” 文件中 “useCaseConfig” 部分下的 “userConfig” 值更改爲指向 MongoDB 而不是 MySql

  3. 在 “appConfig.go” 中爲 MongoDB 創建一個新的結構類型

  4. 在 “configValidator.go” 中爲 MongoDB 添加一個新常量並創建校驗規則。

  5. 在 “datastorefactory” 包中創建一個新的 MongoDB 工廠(MongoDB factory),並在 “datstoreFactory.go” 的“dbFactoryBuilderMap”中爲 MongoDB 添加一個新條目。

  6. 在 “userdata” 下創建一個新文件夾“mongodb”,並添加 MongoDB 實現的代碼。

通過當前的設計,大大減少了需求變化帶來的影響。整個代碼修改沒有涉及業務邏輯代碼。更改僅涵蓋數據服務層和應用程序容器,“用例”或 “模型” 層沒有任何更改。對於數據服務層(步驟 6),我們只爲 MongoDB 添加新代碼,並且沒有更改任何現有的 MySql 代碼。

通過步驟 1 到 5,我們對容器(依賴注入)進行了更改以將 MongoDB 注入到應用程序中,這部分更改了現有代碼,但只觸及了類型創建部分,其他一切代碼都完好無損。

改變用戶註冊用例(registration use case)調用另一個 RESTFul 服務:

其次,假設隨着功能增多,應用程序變得越來越大,你決定將部分功能拆分爲另一個微服務,例如支付服務。現在,你的代碼需要調用另一個微服務,它是用 RESTFul 協議中實現的。以下是步驟:

  1. 在 “appConfig [type] .yaml” 文件中爲 RESTFul 配置添加新條目

  2. 將 “useCaseConfig” 部分下的 “userConfig” 值更改爲指向 RESTFul 配置

  3. 在 “appConfig.go” 中爲 RESTFul 用戶配置創建新的結構類型

  4. 在 “configValidator.go” 中爲 RESTFul 添加一個新常量並創建校驗規則。

  5. 在 “datastorefactory” 子包中創建一個新的 RESTFul 工廠

  6. 將新的 RESTFul 數據接口添加到 “RegistrationUseCase” 結構中,並修改 “registrationFactory.go” 爲其創建具體類型。

  7. 在 “adaptor” 下創建一個新文件夾,併爲 RESTFul 支付服務創建代碼。

通過步驟 1 到 6,我們對容器(依賴注入)進行了更改,以將 RESTFul 注入到程序中,此部分將觸及現有代碼。但是通過把更改限制在只對容器,它大大降低了修改的影響,並保護業務邏輯不會被意外更改。第 7 步是 RESTFul 服務的真正實現。

設計的成本:

接下來,讓我們評估設計的成本。

  1. 爲用例(usecase)層創建接口

  2. 爲數據服務層(dataservice)創建接口

  3. 創建調用其他微服務的接口

  4. 創建程序容器以執行依賴注入

步驟 1 到 3 幾乎沒有額外的工作,對於第 3 步,你可能無法繞過。

第 4 步有一定的工作量,並且比較複雜性。這是基於接口編程的結果。每個函數都通過接口調用另一個函數,但是你需要一個地方來創建具體的類型,那就是應用程序容器,其中所有的複雜性都在其中。大多數複雜性來自於我們希望簡化創建新類型帶來的工作,因此容器必須足夠靈活以適應新類型的加入。

如果你的程序不會引入很多新類型,或者你寧願將來花費更多時間但想現在節省一些時間,那麼你可以通過以下步驟使其更加簡單。首先,如果你不需要靈活地切換到另一個日誌記錄器,請刪除 “logger” 包。其次,刪除 “config” 包。這樣你不需從 YAML 文件中讀取配置,但是你也失去了通過配置文件更改應用程序行爲的靈活性。第三,你甚至可以刪除工廠方法模式。但是,你還將失去上述所有優勢,並且可能會在進行技術更改時冒險破壞業務邏輯的風險。

配置管理:

某些修改的複雜性來自需要從文件中讀取配置。 它是爲了將來可以從配置服務器(configuration server)(管理應用程序配置的程序)讀取配置做準備。 在微服務環境(特別是 Docker 或 Kubernetes 環境)中,服務器 URL 是動態生成和銷燬的,無法在靜態文件中進行管理。 我認爲動態加載應用程序配置的功能是必須的而不是可有可無的。 使用當前的設計,我可以輕鬆地將 “appConfig.go” 更改爲使用 Viper¹²,它支持配置管理。

結論:

當前的設計爲程序增加了一些複雜性,但在動態部署(docker 或 Kubernetes)環境中可能無法避免其中的一些。 總的來說,你可以從這些額外的工作中獲得很大的好處,所以我不認爲這個設計是過度的。

源程序:

完整的源程序鏈接 github

索引:

[1]The Clean Code Blog

[2]S.O.L.I.D is for the first five object-oriented design (OOD) principles introduced by Robert C. Martin, popularly known as Uncle Bob and the acronym is introduced later by Michael Feathers

[3]SOLID Go Design

[4]IoC Container (Dependency Injection)

[5]Go at Google: Language Design in the Service of Software Engineering

[6]Is Go An Object Oriented Language?

[7]Interface-based programming

[8] Go Microservice with Clean architecture: Dependency Injection

[9]Open–closed principle

[10]Data access object

[11]Go Microservice with Clean Architecture: Application Design

[12]viper

不堆砌術語,不羅列架構,不迷信權威,不盲從流行,堅持獨立思考

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://blog.csdn.net/weixin_38748858/article/details/103761028