我最近写了一个Go微服务应用程序,这个程序的设计来自三个灵感:
- 清晰架构"Clean Architecture"1 and SOLID (面向对象设计)2 设计 原则3
- Spring的容器技术(Spring’s application context)?
- Go的简洁设计? 特别是 Go的面向对象的设计?
我来自Java背景,对前两个设计思想非常熟悉。在学习了Go之后,我逐渐认同了Go的简单风格。粗略来说,有两种不同的编程风格,一种是面向对象的, 它强调设计; 另一种是非面向对象的,它信奉用最简单的代码来实现用户需要的功能,无需预先设计。 Go更接近第二阵营,尽管它有一些面向对象的功能。 Go的编程思路为我提供了一个重新评估面向对象编程的新视角,并影响了我的编码风格。结果是我只在必要时才进行面向对象的设计,而我更倾向于使用更简单的解决方案而不是完美的方案。
设计原则:
- 基于接口编程(Programming on interface)?
本程序有三个主要业务层,用例(usecase),数据服务(dataservice)和域模型(model),其中只有域模型没有接口,因为没有必要。 当你访问外部服务时,你可以通过接口进行访问。
// 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。 调用函数不需要知道返回的具体结构,因为接口封装了它需要的所有信息。 这使你可以非常灵活地将返回的结构替换为另一个结构,而不会影响调用函数。
- 用工厂方法模式(factory method pattern)通过依赖注入(Dependency Injection)创建具体类型.
程序容器负责创建具体类型并将其注入函数。 我将在 “依赖注入(Dependency Injection)”?中进行详细解释.
- 建立正确的依赖关系
它意味着以下内容:
- 程序中的各层或组件都有自己的单独的包。 接口在顶级包中定义,具体类型隐藏在子包中。
- 不同层之间仅依赖于接口而不依赖于具体类型
- 从顶层向下的依赖层次是:“用例”,“数据服务”和“模型”。
- 开闭原则(Open-close principle)?
这是我最喜欢的设计原则。 它要求你在需要添加新功能时,不要修改现有代码,而是添加新代码。 实现它的方法是使用上面讲到的#1和#2。 这个原则有许多很好的现实世界的例子,例如,数据访问对象(DAO)1?。 好处是你不会无意中搞乱现有代码,因为只添加新代码,这将大大减少测试工作量。
通常来说有两种类型的需求变更,业务逻辑变更和技术方案变更。 在编写业务代码时,你不希望关注数据是来自MongoDB还是MySQL还是微服务。 在进行技术修改时,最大的噩梦是意外破坏业务逻辑。 一个好的设计将这两种类型的编码在程序中分开,让你一次只关注一个。
一般来说,技术方案变更不会像业务逻辑变化那样频繁发生,但随着微服务的普及,新技术将被更快地采用,这将加速技术变更。
设计带来的好处: 以下是几个示例,向你展示当需求变更时需要对程序进行的改动。 如果你看不太懂本节,可能需要先阅读“程序设计11,它将为你提供程序结构的描述。
从MySQL改成MongoDB: 首先,假设我们需要将域模型“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添加一个新条目。
- 【#|清晰架构(Clean Architecture)的Go微服务: 设计原则】在“userdata”下创建一个新文件夹“mongodb”,并添加MongoDB实现的代码。
通过步骤1到5,我们对容器(依赖注入)进行了更改以将MongoDB注入到应用程序中,这部分更改了现有代码,但只触及了类型创建部分,其他一切代码都完好无损。
改变用户注册用例(registration use case)调用另一个RESTFul服务: 其次,假设随着功能增多,应用程序变得越来越大,你决定将部分功能拆分为另一个微服务,例如支付服务。现在,你的代码需要调用另一个微服务,它是用RESTFul协议中实现的。以下是步骤:
- 在“appConfig [type] .yaml”文件中为RESTFul配置添加新条目
- 将“useCaseConfig”部分下的“userConfig”值更改为指向RESTFul配置
- 在“appConfig.go”中为RESTFul用户配置创建新的结构类型
- 在“configValidator.go”中为RESTFul添加一个新常量并创建校验规则。
- 在“datastorefactory”子包中创建一个新的RESTFul工厂
- 将新的RESTFul数据接口添加到“RegistrationUseCase”结构中,并修改“registrationFactory.go”为其创建具体类型。
- 在“adaptor”下创建一个新文件夹,并为RESTFul支付服务创建代码。
设计的成本: 接下来,让我们评估设计的成本。
- 为用例(usecase)层创建接口
- 为数据服务层(dataservice)创建接口
- 创建调用其他微服务的接口
- 创建程序容器以执行依赖注入
第4步有一定的工作量,并且比较复杂性。这是基于接口编程的结果。每个函数都通过接口调用另一个函数,但是你需要一个地方来创建具体的类型,那就是应用程序容器,其中所有的复杂性都在其中。大多数复杂性来自于我们希望简化创建新类型带来的工作,因此容器必须足够灵活以适应新类型的加入。
如果你的程序不会引入很多新类型,或者你宁愿将来花费更多时间但想现在节省一些时间,那么你可以通过以下步骤使其更加简单。首先,如果你不需要灵活地切换到另一个日志记录器,请删除“logger”包。其次,删除“config”包。这样你不需从YAML文件中读取配置,但是你也失去了通过配置文件更改应用程序行为的灵活性。第三,你甚至可以删除工厂方法模式。但是,你还将失去上述所有优势,并且可能会在进行技术更改时冒险破坏业务逻辑的风险。
配置管理:
某些修改的复杂性来自需要从文件中读取配置。 它是为了将来可以从配置服务器(configuration server)(管理应用程序配置的程序)读取配置做准备。 在微服务环境(特别是Docker或Kubernetes环境)中,服务器URL是动态生成和销毁的,无法在静态文件中进行管理。 我认为动态加载应用程序配置的功能是必须的而不是可有可无的。 使用当前的设计,我可以轻松地将“appConfig.go”更改为使用Viper12,它支持配置管理。
结论: 当前的设计为程序增加了一些复杂性,但在动态部署(docker或Kubernetes)环境中可能无法避免其中的一些。 总的来说,你可以从这些额外的工作中获得很大的好处,所以我不认为这个设计是过度的。
源程序: 完整的源程序链接 github。
索引: [1]The Clean Code Blog
[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
[3]SOLID Go Design
[4]IoC Container ( Dependency Injection)
[5]Go at Google: Language Design in the Service of Software Engineering
[6]Is Go An Object Oriented Language?
[7]Interface-based programming
[8] Go Microservice with Clean architecture: Dependency Injection
[9]Open–closed principle
[10]Data access object
[11]Go Microservice with Clean Architecture: Application Design
[12]viper
不堆砌术语,不罗列架构,不迷信权威,不盲从流行,坚持独立思考
推荐阅读
- 数据结构和算法|LeetCode 的正确使用方式
- #|7.分布式事务管理
- #|算法设计与分析(Java实现)——贪心算法(集合覆盖案例)
- #|算法设计与分析(Java实现)—— 动态规划 (0-1 背包问题)
- #|阿尔法点亮LED灯(一)汇编语言
- #|Multimedia
- #|ARM裸机开发(汇编LED灯实验(I.MX6UL芯片))
- 基础课|使用深度优先搜索(DFS)、广度优先搜索(BFS)、A* 搜索算法求解 (n^2 -1) 数码难题,耗时与内存占用(时空复杂度)对比(附((n^2 - 1) 数码问题控
- #|学习笔记 | Ch05 Pandas数据清洗 —— 缺失值、重复值、异常值
- win10|搏一搏 单车变摩托,是时候捣鼓一下家中的小米电视机啦。