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

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

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

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

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

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

    // sqlUserDataServiceFactory is a empty receiver for Build method
    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。 調用函數不須要知道返回的具體結構,由於接口封裝了它須要的全部信息。 這使你能夠很是靈活地將返回的結構替換爲另外一個結構,而不會影響調用函數。docker

  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](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)

[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](http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)

[3][SOLID Go Design](https://dave.cheney.net/2016/08/20/solid-go-design)

[4][IoC Container ( Dependency Injection)](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans)

[5][Go at Google: Language Design in the Service of Software Engineering](https://talks.golang.org/2012/splash.article)

[6][Is Go An Object Oriented Language?](https://spf13.com/post/is-go-object-oriented/)

[7][Interface-based programming](https://en.wikipedia.org/wiki/Interface-based_programming)

[8] Go Microservice with Clean architecture: Dependency Injection

[9][Open–closed principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle)

[10][Data access object](https://en.wikipedia.org/wiki/Data_access_object)

[11][Go Microservice with Clean Architecture: Application Design](http://www.javashuo.com/article/p-onddrolw-mt.html)

[12][viper](https://github.com/spf13/viper)

相關文章
相關標籤/搜索