清晰架构(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

不堆砌术语,不罗列架构,不迷信权威,不盲从流行,坚持独立思考

相关文章
相关标签/搜索