清晰架構(Clean Architecture)的Go微服務: 程序容器(Application Container)

清晰架構(Clean Architecture)的一個理念是隔離程序的框架,使框架不會接管你的應用程序,而是由你決定什麼時候何地使用它們。在本程序中,我特地不在開始時使用任何框架,所以我能夠更好地控制程序結構。只有在整個程序結構佈局完成以後,我纔會考慮用某些庫替換本程序的某些組件。這樣,引入的框架或第三方庫的影響就會被正確的依賴關係所隔離。目前,除了logger,數據庫,gRPC和Protobuf(這是沒法避免的)以外,我只使用了兩個第三方庫ozzo-validation¹和YAML²,而其餘全部庫都是Go的標準庫。git

你可使用本程序做爲構建應用程序的基礎。你可能會問,那麼本框架豈不是要接管整個應用程序嗎?是的。但事實是,不管是你自建框架仍是引進第三方框架,你都須要一個基本框架做爲構建應用程序的基礎。該基礎須要具備正確的依賴性和可靠的設計,而後你能夠決定是否引入其餘庫。你固然能夠本身創建一個框架,但你最終可能會花費大量的時間和精力來完善它。你也可使用本程序做爲起點,而不是構建本身的項目,從而爲你節省時間和精力。github

程序容器是項目中最複雜的部分,是將應用程序的不一樣部分粘合在一塊兒的關鍵組件。本程序的其餘部分是直截了當且易於理解的,但這一部分不是。好消息是,一旦你理解了這一部分,那麼整個程序就都在掌控之中。golang

容器包(「container」 package)的組成部分:

容器包由五部分組成:sql

  1. 「容器」(「container」)包:它負責建立具體類型並將它們注入其餘文件。 頂級包中只有一個文件「container.go」,它定義容器的接口。

    file

  2. 「servicecontainer」子包:容器接口的實現。 只有一個文件「serviceContainer.go」,這是「容器」包的關鍵。 如下是代碼。 它的起點是「InitApp」,它從文件中讀取配置數據並設置日誌記錄器(logger)。數據庫

    type ServiceContainer struct {
        FactoryMap map[string]interface{}
        AppConfig  *config.AppConfig
    }
    
    func (sc *ServiceContainer) InitApp(filename string) error {
        var err error
        config, err := loadConfig(filename)
        if err != nil {
            return errors.Wrap(err, "loadConfig")
        }
        sc.AppConfig = config
        err = loadLogger(config.Log)
        if err != nil {
            return errors.Wrap(err, "loadLogger")
        }
    
        return nil
    }
    
    // loads the logger
    func loadLogger(lc config.LogConfig) error {
        loggerType := lc.Code
        err := logFactory.GetLogFactoryBuilder(loggerType).Build(&lc)
        if err != nil {
            return errors.Wrap(err, "")
        }
        return nil
    }
    
    // loads the application configurations
    func loadConfig(filename string) (*config.AppConfig, error) {
    
        ac, err := config.ReadConfig(filename)
        if err != nil {
            return nil, errors.Wrap(err, "read container")
        }
        return ac, nil
    }
  3. 「configs」子包:負責從YAML文件加載程序配置,並將它們保存到「appConfig」結構中以供容器使用。

    file

  4. 「logger」子包:它裏面只有一個文件「logger.go」,它提供了日誌記錄器接口和一個「Log」變量來訪問日誌記錄器。 由於每一個文件都須要依賴記錄,因此它須要一個獨立的包來避免循環依賴。

    file

  5. 最後一部分是不一樣類型的工廠(factory)。

    它的內部分層與應用層分層相匹配。 對於「usecase」和「dataservice」層,有「usecasefactory」和「dataservicefactory」。 另外一個工廠是「datastorefactory」,它負責建立底層數據處理連接。 由於數據提供者能夠是gRPC或除數據庫以外的其餘類型的服務,因此它被稱爲「datastorefactry」而不是「databasefactory」。 日誌記錄組件(logger)也有本身的工廠。segmentfault

用例工廠(Use Case Factory):緩存

對於每一個用例,例如「registration」,接口在「usecase」包中定義,但具體類型在「usecase」包下的「registration」子包中定義。 此外,容器包中有一個對應的工廠負責建立具體的用例實例。 對於「註冊(registration)」用例,它是「registrationFactory.go」。 用例與用例工廠之間的關係是一對一的。 用例工廠負責建立此用例的具體類型(concrete type)並調用其餘工廠來建立具體類型所需的成員(member in a struct)。 最低級別的具體類型是sql.DBs和gRPC鏈接,它們須要被傳遞給持久層,這樣才能訪問數據庫中的數據。服務器

若是Go支持泛型,你能夠建立一個通用工廠來構建不一樣類型的實例。 如今,我必須爲每一層建立一個工廠。 另外一個選擇是使用反射(refection),但它有很多問題,所以我沒有采用。架構

「Registration」 用例工廠(Use Case Factory):app

每次調用工廠時,它都會構建一個新類型。如下是「註冊(Registration)」用例建立具體類型的代碼。 它是工廠方法模式(factory method pattern)的典型實現。 若是你想了解有關如何在Go中實現工廠方法模式的更多信息,請參閱此處³.

// Build creates concrete type for RegistrationUseCaseInterface
func (rf *RegistrationFactory) Build(c container.Container, appConfig *config.AppConfig, key string) (UseCaseInterface, error) {
    uc := appConfig.UseCase.Registration
    udi, err := buildUserData(c, &uc.UserDataConfig)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    tdi, err := buildTxData(c, &uc.TxDataConfig)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    ruc := registration.RegistrationUseCase{UserDataInterface: udi, TxDataInterface: tdi}

    return &ruc, nil
}

func buildUserData(c container.Container, dc *config.DataConfig) (dataservice.UserDataInterface, error) {
    dsi, err := dataservicefactory.GetDataServiceFb(dc.Code).Build(c, dc)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    udi := dsi.(dataservice.UserDataInterface)
    return udi, nil
}

數據存儲工廠(Data store factory):

「註冊(Registration)」用例須要經過數據存儲工廠建立的數據庫連接來訪問數據庫。 全部代碼都在「datastorefactory」子包中。 我詳細解釋了數據存儲工廠如何工做,請看這篇文章依賴注入(Dependency Injection)

數據存儲工廠的當前實現支持兩個數據庫和一個微服務,MySql和CouchDB,以及gRPC緩存服務; 每一個實現都有本身的工廠文件。 若是引入了新數據庫,你只需添加一個新的工廠文件,並在如下代碼中的「dsFbMap」中添加一個條目。

// To map "database code" to "database interface builder"
// Concreate builder is in corresponding factory file. For example, "sqlFactory" is in "sqlFactory".go
var dsFbMap = map[string]dsFbInterface{
    config.SQLDB:      &sqlFactory{},
    config.COUCHDB:    &couchdbFactory{},
    config.CACHE_GRPC: &cacheGrpcFactory{},
}

// DataStoreInterface serve as a marker to indicate the return type for Build method
type DataStoreInterface interface{}

// The builder interface for factory method pattern
// Every factory needs to implement Build method
type dsFbInterface interface {
    Build(container.Container, *config.DataStoreConfig) (DataStoreInterface, error)
}

//GetDataStoreFb is accessors for factoryBuilderMap
func GetDataStoreFb(key string) dsFbInterface {
    return dsFbMap[key]
}

如下是MySql數據庫工廠的代碼,它實現了上面的代碼中定義的「dsFbInterface」。 它建立了MySql數據庫連接。

容器內部有一個註冊表(registry),用做數據存儲工廠建立的連接(如DB或gRPC鏈接)的緩存,它們在整個應用程序建立一次。 不管什麼時候須要它們,需首先從註冊表中檢索它,若是沒有找到,則建立一個新的並將其放入註冊表中。 如下是「Build」代碼。

// sqlFactory is receiver for Build method
type sqlFactory struct{}

// implement Build method for SQL database
func (sf *sqlFactory) Build(c container.Container, dsc *config.DataStoreConfig) (DataStoreInterface, error) {
    key := dsc.Code
    //if it is already in container, return
    if value, found := c.Get(key); found {
        sdb := value.(*sql.DB)
        sdt := databasehandler.SqlDBTx{DB: sdb}
        logger.Log.Debug("found db in container for key:", key)
        return &sdt, nil
    }

    db, err := sql.Open(dsc.DriverName, dsc.UrlAddress)
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    // check the connection
    err = db.Ping()
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    dt := databasehandler.SqlDBTx{DB: db}
    c.Put(key, db)
    return &dt, nil

}

Grpc Factory:

對於「listUser」用例,它須要調用gRPC微服務(緩存服務),而建立它的工廠是「cacheFactory.go」。 目前,數據服務的全部連接都是由數據存儲工廠建立的。 如下是gRPC工廠的代碼。 「Build」方法與「SqlFactory」的很是類似。

// DataStoreInterface serve as a marker to indicate the return type for Build method
type DataStoreInterface interface{}

// cacheGrpcFactory is an empty receiver for Build method
type cacheGrpcFactory struct{}

func (cgf *cacheGrpcFactory) Build(c container.Container, dsc *config.DataStoreConfig) 
     (DataStoreInterface, error) {
    key := dsc.Code
    //if it is already in container, return
    if value, found := c.Get(key); found {
        return value.(*grpc.ClientConn), nil
    }
    //not in map, need to create one
    logger.Log.Debug("doesn't find cacheGrpc key=%v need to created a new one\n", key)

    conn, err := grpc.Dial(dsc.UrlAddress, grpc.WithInsecure())
    if err != nil {
        return nil, errors.Wrap(err, "")
    }
    c.Put(key, conn)
    return conn, err
}

Logger factory:

Logger有本身的子包名爲「loggerfactory」,其結構與「datastorefactory」子包很是類似。 「logFactory.go」定義了日誌記錄器工廠構建器接口(builder interface)和映射(map)。 每一個單獨的日誌記錄器都有本身的工廠文件。 如下是日誌工廠的代碼:

// logger mapp to map logger code to logger builder
var logfactoryBuilderMap = map[string]logFbInterface{
    config.ZAP:    &ZapFactory{},
    config.LOGRUS: &LogrusFactory{},
}

// interface for logger factory
type logFbInterface interface {
    Build(*config.LogConfig) error
}

// accessors for factoryBuilderMap
func GetLogFactoryBuilder(key string) logFbInterface {
    return logfactoryBuilderMap[key]
}

如下是ZAP工廠的代碼。 它相似於數據存儲工廠。 只有一個區別。 因爲記錄器建立功能僅被調用一次,所以不須要註冊表。

// receiver for zap factory
type ZapFactory struct{}

// build zap logger
func (mf *ZapFactory) Build(lc *config.LogConfig) error {
    err := zap.RegisterLog(*lc)
    if err != nil {
        return errors.Wrap(err, "")
    }
    return nil
}

配置文件:

配置文件使你能夠全面瞭解程序的總體結構:

file

上圖顯示了文件的前半部分。 第一部分是它支持的數據庫配置; 第二部分是帶有gRPC的微服務; 第三部分是它支持的日誌記錄器; 第四部分是本程序在運行時使用的日誌記錄器

下圖顯示了文件的後半部分。 它列出了應用程序的全部用例以及每一個用例所需的數據服務。

file

配置文件中應保存哪些數據?

不一樣的組件具備不一樣的配置項,一些組件可能具備許多配置項,例如日誌記錄器。 咱們不須要在配置文件中保存全部配置項,這可能使其太大而沒法管理。 一般咱們只須要保存須要在運行時更改的選項或者能夠在不一樣環境中(dev, prod, qa)值不一樣的選項。

設計是如何進化的?

容器包裏彷佛有太多東西,問題是咱們是否須要全部這些?若是你不須要全部功能,咱們固然能夠簡化它。當我開始建立它時,它很是簡單,我不斷添加功能,最終它才愈來愈複雜。

最開始時,我只是想使用工廠方法模式來建立具體類型,沒有日誌記錄,沒有配置文件,沒有註冊表。

我從用例和數據存儲工廠開始。最初,對於每一個用例,都會建立一個新的數據庫連接,這並不理想。所以,我添加了一個註冊表來緩存全部鏈接,以確保它們只建立一次。

而後我發現(我從這裏得到了一些靈感⁵)將全部配置信息放在一個文件中進行集中管理是個好主意,這樣我就能夠在不改變代碼的狀況下進行更改。
我建立了一個YAML文件(appConfig [type] .yaml)和「appConfig.go」來將文件中的內容加載到應用程序配置結構(struct) - 「appConfig」中並將其傳遞給工廠構建器(factory builder)。 「[type]」能夠是「prod」,「dev」,「test」等。配置文件只加載一次。目前,它沒有使用任何第三方庫,但我想未來切換到Vipe⁶,由於它能夠支持從配置服務器中動態從新加載程序配置。要切換到Vipe,我只須要更改一個文件「appConfig.go」。

對於日誌記錄,整個程序我只想要一個logger實例,這樣我就能夠爲整個程序設置相同的日誌配置。我在容器內建立了一個日誌記錄器包。我還嘗試了不一樣的日誌庫來肯定哪個是最好的,而後我建立了一個日誌工廠,以便未來更容易添加新的日誌記錄器。有關詳細信息,請閱讀日誌管理⁷。

源程序:

完整的源程序連接 github: https://github.com/jfeng45/se...

索引:

[1] ozzo-validation

[2] YAML support for the Go language

[3]Golang Factory Method

[4]Go Microservice with Clean Architecture: Dependency Injection

[5] How I pass around shared resources (databases, configuration, etc) within Golang projects

[6]viper

[7]Go Microservice with Clean Architecture: Application Logging

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

相關文章
相關標籤/搜索