我最近写了一个Go微服务应用程序,这个程序的设计来自三个灵感:html
清晰架构"Clean Architecture"¹ and SOLID (面向对象设计)² 设计 原则³mysql
Go的简洁设计⁵ 特别是 Go的面向对象的设计⁶github
我使用Spring的基于接口的编程和依赖注入(Dependency Injection)来实现Bob Martin的清晰架构(Clean Architecture),并遵循了Go的简单编程风格。当它们之间存在冲突时,进行了取舍。我只采用了Clean Architecture的设计原则(主要是SOLID),所以实现的细节可能与其余SOLID实现不一样。golang
我来自Java背景,对前两个设计思想很是熟悉。在学习了Go以后,我逐渐认同了Go的简单风格。粗略来讲,有两种不一样的编程风格,一种是面向对象的, 它强调设计;另外一种是非面向对象的,它信奉用最简单的代码来实现用户须要的功能,无需预先设计。 Go更接近第二阵营,尽管它有一些面向对象的功能。 Go的编程思路为我提供了一个从新评估面向对象编程的新视角,并影响了个人编码风格。结果是我只在必要时才进行面向对象的设计,而我更倾向于使用更简单的解决方案而不是完美的方案。spring
基于接口编程(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
用工厂方法模式(factory method pattern)经过依赖注入(Dependency Injection)建立具体类型.编程
程序容器负责建立具体类型并将其注入函数。 我将在 “依赖注入(Dependency Injection)”⁸中进行详细解释.
创建正确的依赖关系
它意味着如下内容:这是我最喜欢的设计原则。 它要求你在须要添加新功能时,不要修改现有代码,而是添加新代码。 实现它的方法是使用上面讲到的#1和#2。 这个原则有许多很好的现实世界的例子,例如,数据访问对象(DAO)¹⁰。 好处是你不会无心中搞乱现有代码,由于只添加新代码,这将大大减小测试工做量。
与Java中的相似解决方案相比,因为Go的语言自己的简单设计,本程序中的代码量要少不少,也很是简洁。 可是对于来自其余编程语言(特别是动态语言如PHP,Ruby)的人来讲,这个程序的设计可能有些重。 我也问了本身一样的问题。 为了获得答案,须要比较成本和收益以得出最终结论。
一般来讲有两种类型的需求变动,业务逻辑变动和技术方案变动。 在编写业务代码时,你不但愿关注数据是来自MongoDB仍是MySQL仍是微服务。 在进行技术修改时,最大的噩梦是意外破坏业务逻辑。 一个好的设计将这两种类型的编码在程序中分开,让你一次只关注一个。
通常来讲,技术方案变动不会像业务逻辑变化那样频繁发生,但随着微服务的普及,新技术将被更快地采用,这将加速技术变动。
如下是几个示例,向你展现当需求变动时须要对程序进行的改动。 若是你看不太懂本节,可能须要先阅读“程序设计¹¹,它将为你提供程序结构的描述。
首先,假设咱们须要将域模型“User”的持久层从MySQL更改成MongoDB。如下是步骤:
在“appConfig [type] .yaml”文件中添加MongoDB的新配置信息
将“appConfig [type] .yaml”文件中“useCaseConfig”部分下的“userConfig”值更改成指向MongoDB而不是MySql
在“appConfig.go”中为MongoDB建立一个新的结构类型
在“configValidator.go”中为MongoDB添加一个新常量并建立校验规则。
在“datastorefactory”包中建立一个新的MongoDB工厂(MongoDB factory),并在“datstoreFactory.go”的“dbFactoryBuilderMap”中为MongoDB添加一个新条目。
在“userdata”下建立一个新文件夹“mongodb”,并添加MongoDB实现的代码。
经过当前的设计,大大减小了需求变化带来的影响。整个代码修改没有涉及业务逻辑代码。更改仅涵盖数据服务层和应用程序容器,“用例”或“模型”层没有任何更改。对于数据服务层(步骤6),咱们只为MongoDB添加新代码,而且没有更改任何现有的MySql代码。
经过步骤1到5,咱们对容器(依赖注入)进行了更改以将MongoDB注入到应用程序中,这部分更改了现有代码,但只触及了类型建立部分,其余一切代码都无缺无损。
其次,假设随着功能增多,应用程序变得愈来愈大,你决定将部分功能拆分为另外一个微服务,例如支付服务。如今,你的代码须要调用另外一个微服务,它是用RESTFul协议中实现的。如下是步骤:
在“appConfig [type] .yaml”文件中为RESTFul配置添加新条目
将“useCaseConfig”部分下的“userConfig”值更改成指向RESTFul配置
在“appConfig.go”中为RESTFul用户配置建立新的结构类型
在“configValidator.go”中为RESTFul添加一个新常量并建立校验规则。
在“datastorefactory”子包中建立一个新的RESTFul工厂
将新的RESTFul数据接口添加到“RegistrationUseCase”结构中,并修改“registrationFactory.go”为其建立具体类型。
在“adaptor”下建立一个新文件夹,并为RESTFul支付服务建立代码。
经过步骤1到6,咱们对容器(依赖注入)进行了更改,以将RESTFul注入到程序中,此部分将触及现有代码。可是经过把更改限制在只对容器,它大大下降了修改的影响,并保护业务逻辑不会被意外更改。第7步是RESTFul服务的真正实现。
接下来,让咱们评估设计的成本。
为用例(usecase)层建立接口
为数据服务层(dataservice)建立接口
建立调用其余微服务的接口
建立程序容器以执行依赖注入
步骤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)