在清晰架构(Clean Architecture)中,应用程序的每一层(用例,数据服务和域模型)仅依赖于其他层的接口而不是具体类型。 在运行时,程序容器¹负责创建具体类型并将它们注入到每个函数中,它使用的技术称为依赖注入²。 以下是要求。

容器包的依赖关系:

  1. 容器包是唯一依赖于具体类型和许多外部库的包,因为它需要创建具体类型。 本程序中的所有其他软件包主要仅依赖于接口。

  2. 外部库可以包括DB和DB连接,gRPC连接,HTTP连接,SMTP服务器,MQ等。

  3. #2中提到的具体类型的资源链接只需要创建一次并放入注册表中,所有后来的请求都将从注册表中检索它们。

  4. 只有用例层需要访问并依赖于容器包。

依赖注入的核心是工厂方法模式(factory method pattern)。

工厂方法模式(Factory Method Pattern):

实现工厂方法模式并不困难,这里³描述了是如何在Go中实现它的。困难的部分是使其可扩展,即如何避免在添加新工厂时修改代码。

处理新工厂的方式有很多种,下面是常见的三种:

#1不是一个好选择,因为你需要在添加新类型时修改现有代码。 #3是最好的,因为添加新工厂时现有代码不需更改。在Java中,我会使用#3,因为Java具有非常优雅的反射实现。你可以执行类似“(Animal)Class.forName(”className“)。newInstance()”的操作,即你可以将类的名称作为函数中的字符串参数传递进来,并通过反射从中创建一个类型的新实例,然后将结构转换为适当的类型(可能是它的一个超级类型(super type),这是非常强大的。由于Go的反射不如Java,#3不是一个好选择。在Go中,由反射创建的实例是反射类型而不是实际类型,并且你无法在反射类型和实际类型之间转换类型,它们处于两个不同的世界中,这使得Go中的反射难以使用。所以我选择#2,它比#1好,但是在添加新类型时需要更改少部分代码。

以下是数据存储工厂的代码。它有一个“dsFbInterface”,其中有一个“Build”函数需要由每个数据存储工厂实现。 “Build”是工厂的关键部分。 “dsFbMap”是每个数据库(或gRPC)的代码(code)与实际工厂之间的映射。这是添加数据库时需要更改的部分。


  1. // To map "database code" to "database interface builder"
  2. // Concreate builder is in corresponding factory file. For example, "sqlFactory" is in "sqlFactory".go
  3. var dsFbMap = map[string]dsFbInterface{
  4. config.SQLDB: &sqlFactory{},
  5. config.COUCHDB: &couchdbFactory{},
  6. config.CACHE_GRPC: &cacheGrpcFactory{},
  7. }
  8. // DataStoreInterface serve as a marker to indicate the return type for Build method
  9. type DataStoreInterface interface{}
  10. // The builder interface for factory method pattern
  11. // Every factory needs to implement Build method
  12. type dsFbInterface interface {
  13. Build(container.Container, *config.DataStoreConfig) (DataStoreInterface, error)
  14. }
  15. //GetDataStoreFb is accessors for factoryBuilderMap
  16. func GetDataStoreFb(key string) dsFbInterface {
  17. return dsFbMap[key]
  18. }

以下是“sqlFactory”的程序,它实现了上面的代码中定义的“dsFbInterface”。 它为MySql数据库创建数据存储。 在“Build”函数中,它首先从注册表中检索数据存储(MySql),如果找到,则返回,否则创建一个新的并将其放入注册表。

因为注册表可以存储任何类型的数据,所以我们需要在检索后将返回值转换为适当的类型(*sql.DB)。 “databasehandler.SqlDBTx”是实现“SqlGdbc”接口的具体类型。 它的创建是为了支持事务管理。 代码中调用“sql.Open()”来打开数据库连接,但它并没有真正执行任何连接数据库的操作。 因此,需调用“db.Ping()”去访问数据库以确保数据库正在运行。


  1. // sqlFactory is receiver for Build method
  2. type sqlFactory struct{}
  3. // implement Build method for SQL database
  4. func (sf *sqlFactory) Build(c container.Container, dsc *config.DataStoreConfig) (DataStoreInterface, error) {
  5. key := dsc.Code
  6. //if it is already in container, return
  7. if value, found := c.Get(key); found {
  8. sdb := value.(*sql.DB)
  9. sdt := databasehandler.SqlDBTx{DB: sdb}
  10. logger.Log.Debug("found db in container for key:", key)
  11. return &sdt, nil
  12. }
  13. db, err := sql.Open(dsc.DriverName, dsc.UrlAddress)
  14. if err != nil {
  15. return nil, errors.Wrap(err, "")
  16. }
  17. // check the connection
  18. err = db.Ping()
  19. if err != nil {
  20. return nil, errors.Wrap(err, "")
  21. }
  22. dt := databasehandler.SqlDBTx{DB: db}
  23. c.Put(key, db)
  24. return &dt, nil
  25. }

数据服务工厂(Data service factory)

数据服务层使用工厂方法模式来创建数据服务类型。 可以有不同的策略来应用此模式。 在构建数据服务工厂时,我使用了三种不同的策略,每种策略都有其优缺点。 我将详细解释它们,以便你可以决定在那种情况下使用哪一个。

基础工厂(Basic factory)

最简单的是“cacheGrpcFactory”,因为数据存储只有一个底层实现(即gRPC),所以只创建一个工厂就行了。

二级工厂(Second level factory)

对于数据库工厂,情况并非如此。 因为我们需要每个数据服务同时支持多个数据库,所以需要二级工厂,这意味着对于每种数据服务类型,例如“UserDataService”,我们需要为每个支持的数据库使用单独的工厂。 现在,由于有两个数据库,我们需要两个工厂。

你可以从上面的图像中看到,我们需要四个文件来完成“UserDataService”,其中“userDataServiceFactoryWrapper.go”是在“userdataservicefactory”文件夹中调用实际工厂的封装器(wrapper)。 “couchdbUserDataServiceFactory.go”和“sqlUserDataServiceFactory.go”是CouchDB和MySql数据库的真正工厂。 “userDataServiceFactory.go”定义了接口。 如果你有许多数据服务,那么你将创建许多类似代码。

简化工厂(Simplified factory)

有没有办法简化它? 有的,这是第三种方式,但也带来一些问题。 以下是“courseDataServiceFactory.go”的代码。 你可以看到只需一个文件而不是之前的四个文件。 代码类似于我们刚才谈到的“userDataServiceFactory”。那么它是如何如何简化代码的呢?

关键是为底层数据库链接创建统一的接口。 在“courseDataServiceFactory.go”中,可以在调用“dataStoreFactory”之后获得底层数据库链接统一接口,并将“CourseDataServiceInterface”的DB设置为正确的“gdbc”(只要它实现“gdbc”接口,它可以是任何数据库链接)。


  1. var courseDataServiceMap = map[string]dataservice.CourseDataInterface{
  2. config.COUCHDB: &couchdb.CourseDataCouchdb{},
  3. config.SQLDB: &sqldb.CourseDataSql{},
  4. }
  5. // courseDataServiceFactory is an empty receiver for Build method
  6. type courseDataServiceFactory struct{}
  7. // GetCourseDataServiceInterface is an accessor for factoryBuilderMap
  8. func GetCourseDataServiceInterface(key string) dataservice.CourseDataInterface {
  9. return courseDataServiceMap[key]
  10. }
  11. func (tdsf *courseDataServiceFactory) Build(c container.Container, dataConfig *config.DataConfig) (DataServiceInterface, error) {
  12. dsc := dataConfig.DataStoreConfig
  13. dsi, err := datastorefactory.GetDataStoreFb(dsc.Code).Build(c, &dsc)
  14. if err != nil {
  15. return nil, errors.Wrap(err, "")
  16. }
  17. gdbc := dsi.(gdbc.Gdbc)
  18. gdi := GetCourseDataServiceInterface(dsc.Code)
  19. gdi.SetDB(gdbc)
  20. return gdi, nil
  21. }

它的缺点是,对于任何支持的数据库,需要实现以下代码中“SqlGdbc”和“NoSqlGdbc”接口,即使它只使用其中一个,另一个只是空实现(以满足接口要求)并没有被使用。 如果你只有少数几个数据库需要支持,这可能是一个可行的解决方案,否则它将变得越来越难以管理。


  1. // SqlGdbc (SQL Go database connection) is a wrapper for SQL database handler
  2. type SqlGdbc interface {
  3. Exec(query string, args ...interface{}) (sql.Result, error)
  4. Prepare(query string) (*sql.Stmt, error)
  5. Query(query string, args ...interface{}) (*sql.Rows, error)
  6. QueryRow(query string, args ...interface{}) *sql.Row
  7. // If need transaction support, add this interface
  8. Transactioner
  9. }
  10. // NoSqlGdbc (NoSQL Go database connection) is a wrapper for NoSql database handler.
  11. type NoSqlGdbc interface {
  12. // The method name of underline database was Query(), but since it conflicts with the name with Query() in SqlGdbc,
  13. // so have to change to a different name
  14. QueryNoSql(ctx context.Context, ddoc string, view string) (*kivik.Rows, error)
  15. Put(ctx context.Context, docID string, doc interface{}, options ...kivik.Options) (rev string, err error)
  16. Get(ctx context.Context, docID string, options ...kivik.Options) (*kivik.Row, error)
  17. Find(ctx context.Context, query interface{}) (*kivik.Rows, error)
  18. AllDocs(ctx context.Context, options ...kivik.Options) (*kivik.Rows, error)
  19. }
  20. // gdbc is an unified way to handle database connections.
  21. type Gdbc interface {
  22. SqlGdbc
  23. NoSqlGdbc
  24. }

除了上面谈到的那个之外,还有另一个副作用。 在下面的代码中,“CourseDataInterface”中的“SetDB”函数打破了依赖关系。 因为“CourseDataInterface”是数据服务层接口,所以它不应该依赖于“gdbc”接口,这是下面一层的接口。 这是本程序的依赖关系中的第二个缺陷,第一个是在事物管理⁶模块。 目前对它没有好的解决方法,如果你不喜欢它,就不要使用它。 可以创建类似于“userFataServiceFactory”的二级工厂,只是程序较长而已。


  1. import (
  2. "github.com/jfeng45/servicetmpl/model"
  3. "github.com/jfeng45/servicetmpl/tool/gdbc"
  4. )
  5. // CourseDataInterface represents interface for persistence service for course data
  6. // It is created for POC of courseDataServiceFactory, no real use.
  7. type CourseDataInterface interface {
  8. FindAll() ([]model.Course, error)
  9. SetDB(gdbc gdbc.Gdbc)
  10. }

怎样选择?

怎样选择是用简化工厂还是二级工厂?这取决于变化的方向。如果你需要支持大量新数据库,但新的数据服务不多(由新的域模型类型决定),那么选二级工厂,因为大多数更改都会发生在数据存储工厂中。但是如果支持的数据库不会发生太大变化,并且数据服务的数量可能会增加很多,那么选择简化工厂。如果两者都可能增加很多呢?那么只能使用二级工厂,只是程序会比较长。

怎样选择使用基本工厂还是二级工厂?实际上,即使你需要支持多个数据库,但不需同时支持多个数据库,你仍然可以使用基本工厂。例如,你需要从MySQL切换到MongoDB,即使有两个不同的数据库,但在切换后,你只使用MongoDB,那么你仍然可以使用基本工厂。对于基本工厂,当有多种类型时,你需要更改代码以进行切换(但对于二级工厂,你只需更改配置文件),因此如果你不经常更改代码,这是可以忍受的。

备注:上面是我在写这段代码时的想法。但如果现在让我选择,我可能不会使用简化工厂。因为我对程序复杂度有了不同的认识。我依据的原则并没有变,都是要降低代码复杂度。但我以前认为代码越长越复杂,但现在我会加上另外一个维度,就是代码的结构复杂度。二级工厂虽然代码长了很多,但结构简单,只要完成了一个,就可以拷贝出许多,结构几乎一模一样,这样不论读写都非常容易。它的复杂度是线性增加的,而且不会有其他副作用。另外,你可以使用代码生成器等工具来自动生成,以提高效率。而“简化工厂”虽然代码量少了,但结构复杂,它的复杂度增加很快,而且副作用太大,很难管理。

依赖注入(Dependency Injection)库

Go中已经有几个依赖注入库,为什么我不使用它们?我有意在项目初期时不使用任何库,所以我可以更好地控制程序结构,只有在完成整个程序结构布局之后,我才会考虑用外部库替换本程序的某些组件。

我简要地看了几个流行的依赖注入库,一个是来自优步⁷的Dig⁸,另一个是来自谷歌¹⁰的Wire⁹ 。 Dig使用反射,Wire使用代码生成。这两种方法我都不喜欢,但由于Go目前不支持泛型,因此这些是唯一可用的选项。虽然我不喜欢他们的方法,但我不得不承认这两个库的依赖注入功能更全。

我试了一下Dig,发现它没有使代码更简单,所以我决定继续使用当前的解决方案。在Dig中,你为每个具体类型创建“(build)”函数,然后将其注册到容器,最后容器将它们自动连接在一起以创建顶级类型。本程序的复杂性是因为我们需要支持两个数据库实现,因此每个域模型有两个不同的数据库链接和两组不同的数据服务实现。在Dig中没有办法使这部分更简单,你仍然需要创建所有工厂然后把它们注册到容器。当然,你可以使用“if-else”方法来实现工厂,这将使代码更简单,但你以后需要付出更多努力来维护代码。

我的方法简单易用,并且还支持从文件加载配置,但是你需要了解它的原理以扩展它。 Dig提供的附加功能是自动加载依赖关系。如果你的应用程序有很多类型并且类型之间有很多复杂的依赖关系,那么你可能需要切换到Dig或Wire,否则请继续使用当前的解决方案

接口设计

下面是 “userDataServiceFactoryWrapper”的代码.

  1. // DataServiceInterface serves as a marker to indicate the return type for Build method
  2. type DataServiceInterface interface{}
  3. // userDataServiceFactory is a empty receiver for Build method
  4. type userDataServiceFactoryWrapper struct{}
  5. func (udsfw *userDataServiceFactoryWrapper) Build(c container.Container, dataConfig *config.DataConfig)
  6. (DataServiceInterface, error) {
  7. key := dataConfig.DataStoreConfig.Code
  8. udsi, err := userdataservicefactory.GetUserDataServiceFb(key).Build(c, dataConfig)
  9. if err != nil {
  10. return nil, errors.Wrap(err, "")
  11. }
  12. return udsi, nil
  13. }

你可能注意到了“Build()”函数的返回类型是“DataServiceInterface”,这是一个空接口,为什么我们需要一个空接口? 我们可以用“interface {}”替换“DataServiceInterface”吗?

  1. // userDataServiceFactory is a empty receiver for Build method
  2. type userDataServiceFactoryWrapper struct{}
  3. func (udsfw *userDataServiceFactoryWrapper) Build(c container.Container, dataConfig *config.DataConfig)
  4. (interface{}, error) {
  5. ...
  6. }

如果将返回类型从“DataServiceInterface”替换为“interface {}”,结果是相同的。 “DataServiceInterface”的好处是它可以告诉我函数的返回类型,即数据服务接口; 实际上,真正的返回类型是“dataservice.UserDataInterface”,但是“DataStoreInterface”现在已经足够好了,一个小诀窍让生活变得轻松一点。

结论:

程序容器使用依赖注入创建具体类型并将它们注入每个函数。 它的核心是工厂方法模式。 在Go中有三种方法可以实现它,最好的方法是在映射(map)中保存不同的工厂。 将工厂方法模式应用于数据服务层也有不同的方法,它们各自都有利有弊。 你需要根据应用程序的更改方向选择正确的方法。

源程序:

完整的源程序链接 github: https://github.com/jfeng45/servicetmpl

索引:

[1]Go Microservice with Clean Architecture: Application Container

[2] Inversion of Control Containers and the Dependency Injection pattern

[3]]Golang Factory Method

[4]Creating a factory method in Java that doesn’t rely on if-else

[5]Tom Hawtin’s answer

[6]Go Microservice with Clean Architecture: Transaction Support

[7]Dependency Injection in Go

[8]Uber’s dig

[9]Go Dependency Injection with Wire

[10]Google’s Wire: Automated Initialization in Go

清晰架构(Clean Architecture)的Go微服务: 依赖注入(Dependency Injection)的更多相关文章

  1. (转)微服务架构 互联网保险O2O平台微服务架构设计

    http://www.cnblogs.com/Leo_wl/p/5049722.html 微服务架构 互联网保险O2O平台微服务架构设计 关于架构,笔者认为并不是越复杂越好,而是相反,简单就是硬道理也 ...

  2. Java 18套JAVA企业级大型项目实战分布式架构高并发高可用微服务电商项目实战架构

    Java 开发环境:idea https://www.jianshu.com/p/7a824fea1ce7 从无到有构建大型电商微服务架构三个阶段SpringBoot+SpringCloud+Solr ...

  3. 通俗地理解面向服务的架构(SOA)以及微服务之间的关系

    SOA是一种软件的应用架构方法,它基于面向对象,但又不是面向对象,整体上是面向服务的架构.SOA由精确的服务定义.松散的构件服务组成,以及业务流程调用等多个方面形成的一整套架构方法. 这话是不是听起来 ...

  4. Re:从0开始的微服务架构--(二)快速快速体验微服务架构?--转

    原文地址:https://mp.weixin.qq.com/s/QO1QDQWnjHZp8EvGDrxZvw 这是专题的第二篇文章,看看如何搭建一个简单模式的微服务架构. 记得好久之前看到一个大牛说过 ...

  5. 【分布式微服务企业快速架构】SpringCloud分布式、微服务、云架构快速开发平台源码

    鸿鹄云架构[系统管理平台]是一个大型 企业.分布式.微服务.云架构的JavaEE体系快速研发平台,基于 模块化.微服务化.原子化.热部署的设计思想,使用成熟领先的无商业限制的主流开源技术 (Sprin ...

  6. Otto开发初探——微服务依赖管理新利器

    [编者按]时下,Vagrant 被 DevOps 软件开发商广泛作为开发阶段的本地软件开发环境,而在本文,CERT Division高级研究员介绍的 Otto 则是 Vagrant 开发团队 Hash ...

  7. 剑指架构师系列-Struts2构造函数的循环依赖注入

    Struts2可以完成构造函数的循环依赖注入,来看看Struts2的大师们是怎么做到的吧! 首先定义IBlood与BloodImpl类: public interface IBlood { } pub ...

  8. 清晰架构(Clean Architecture)的Go微服务: 设计原则

    我最近写了一个Go微服务应用程序,这个程序的设计来自三个灵感: 清晰架构"Clean Architecture"¹ and SOLID (面向对象设计)² 设计 原则³ Sprin ...

  9. 清晰架构(Clean Architecture)的Go微服务

    我用Go和gRPC创建了一个微服务项目,并试图找出最好的程序结构,它可以作为我其他项目的模板.我还将程序设计和编程的最佳实践应用于Go Microservice程序,例如清晰架构(Clean Arch ...

随机推荐

  1. uni-app学习记录05-二级联动及获取DOM对象

    <template> <view> <view class="id"> <view class="left"> ...

  2. java 网络编程Socket

    TCP: 通过TCP协议传输,得到的是一个顺序的无差错的数据流. 发送方和接收方的成对的两个socket之间必须建立连接, 以便在TCP协议的基础上进行通信,当一个socket(通常都是server ...

  3. no_expand优化案例

    bond 来看一个烂语句: select a.*,b.dn from temp_allcrmuser a, phs_smc_user b  where a.USERNUMBER=b.dn  and ( ...

  4. tomcat最大线程数的设置

    Tomcat的server.xml中连接器设置如下 <Connector port="8080" maxThreads="150" minSpareThr ...

  5. 【9103】求n的累加和

    Time Limit: 10 second Memory Limit: 2 MB 问题描述 用高精度方法,求s=1+2+3+...+n的精确值(n以一般整数输入) Input 文件输入仅一行,输入n ...

  6. PowerShell 通过 WMI 获取系统安装的驱动

    本文告诉大家如何通过 WMI 获取用户已经安装的驱动程序 通过下面代码可以获取用户已经安装的驱动程序 Get-WmiObject Win32_SystemDriver | Format-List Ca ...

  7. Latex 公式速查

    本文记录了一些常用的数学公式对应的 Latex 字符,用于快速查找需要的字符 所有的在 Latex 使用的字符公式,都需要放在\(和\),$ 和 $,\begin{math} 和\end{math}之 ...

  8. Nodejs之使用express框架搭建WEB应用

      首先创建一个index.js,在里面引入相关的中间件,如果没有这些中间件,则需要在nodejs里进入index.js所在的文件夹安装这些模块,安装命令:npm install express ex ...

  9. Qt和c/c++connect函数冲突解决方法

    在使用c/c++的connect函数时在前面写::connect()这样就可以解决了

  10. springmvc整合freemarker教程(转)

    1.介绍 我最近喜欢freemarker在网上找了大半天.都没有找到一个简单又容易理解的案例.虽然只是一个模板技术.但是相对刚开始什么都不知道的,很难入手.下面是自学(其实是谷歌和百度的东找西补).写 ...