本文的行文思路
- 提出问题
- 提出初步解决方案
- 优化初步解决方案
- 演进到函数可选项式编程模式
一、配置问题
在Server这个结构体中,我们可以看到:
AddrPortProtocolTimeoutMaxConnstcp301024TLS
所以针对不同可选项的配置,有多种创建Server的函数签名。
因为Go语言不支持函数重载,所以必须用不同的函数名来对应不同的配置可选项。
缺点:
1、太多创建Server的函数,程序员不知道调用哪个好
2、会写很多冗余代码
3、代码的可扩展性不好
二、如何解决
2、1 分离可选项
很认可“六月天天”博客上说的这句话,编程的一大重点,就是“分离变化点和不变点”。
将非必填字段放入Config结构体中,于是Server结构体就变为了
于是我们只需要一个NewServer()的函数,在使用前需要构造Config对象。
ConfignilConfig{}
除了“分离可选项”这种方法外,还有其他的方法吗?
2、2 Builder模式
使用一个builder类来包装Server,然后分两步操作:
1、Create方法根据必填项addr和port参数来创建ServerBuilder实例
2、WithProtocol、WithMaxConn、WithTimeOut、WithTLS方法将函数参数传递给builder类中的Server对象
3、提供Build()方法将builder类中的Server对象暴露出来
于是就可以以如下的方式来使用了
有不少开源代码是采用此种方式进行的。
使用链式的函数调用的方式来构造一个对象,只需要多加一个Builder类。
缺点:
Builder类似乎有点多余,能否省去呢?
如果想省点这个包装的结构体,那么就轮到函数选项登场了。
2、3 函数选项模式
函数选项模式的实现,主要有两种
- 基于闭包实现
- 基于接口实现
2、3、1 基于闭包的实现
1)定义一个函数类型Option
type Option func(*Server)
2)闭关方式定义函数
这里对函数参数并未做校验(感兴趣的小伙伴可以自行实现)
以WithMaxConns函数为例,传入一个参数maxConn,返回一个函数,这个函数会将参数maxConn赋值给Server的对应的MaxConns字段。
- 当我们调用其中的一个函数用WithMaxConns(30)时
- 其返回值是一个func(s *Server) { s.MaxConns = 30 }的函数
3)定义NewServer()函数,
函数参数是必填字段和可选字段(可变参数类型)
首先,NewServer函数的opts是可变参数,可以传递上面的WithProtocol、WithTimeout、WithMaxConns等函数中的一个或多个。
然后,创建Server对象,并填写可选项的默认值,如Timeout超时时间为10秒。
最后,for-loop循环opts可变参数,执行函数option去设置s对象中的各个属性。完毕!
4)测试NewServer()函数
运行结果如下:
2、3、2 基于接口的实现
1)定义接口
Option接口定义了apply(server *Server)方法,然后可选项需要实现接口
2)可选项实现接口
每个可选项都需要实现接口
以可选项protocol的ProtoOption为例:
自定义类型ProtoOption实现了Option接口。
3)封装函数将可选项包装成接口类型
提供WithProtocol函数将string类型的proto转换为ProtoOption类型,方便以Option接口的形式传递给函数使用
4) 定义NewServer函数
for-loop循环opts可变参数,执行接口中的apply发那个发去设置s对象中的各个属性,记得s一定要传递指针,否则无法修改对象属性!
三、总结
本文就配置可选项问题,进行了深入浅出的分析,提出了三种解决方案,分别是
- 分离可选项
- Builder模式
- 函数选项模式
在平时阅读开源项目过程中,以Builder模式和函数选项模式居多,函数选项模式的优点还是挺多的。
优点:
可读性强,将配置都转化成了对应的函数项option
扩展性好,新增参数只需要增加一个对应的方法
缺点:
需编写多个Option函数,代码量会有所增加
参考链接: