前言
在很多优秀的Go语言项目中我们都可以见到选项模式(Options Pattern),例如grpc/grpc-go的NewServer函数、uber-go/zap 包的New函数都用到了选项模式。
使用选项模式,我们可以非常华丽地实现——带许多默认值且可选地修改其中某些值的工厂方法或者函数。这弥补了Go没有给参数设置默认值的语法的问题。大大简化开发者创建一个对象的成本,尤其是在对象拥有众多属性时。
如何带默认参数地创建对象
多个工厂方法
在创建一个对象的时候,如果想要有一些默认值,最朴素地,我们可能会为一个对象准备多个工厂方法:
package client
import "time"
const (
DefaultTimeout = 1 * time.Second
)
type DemoClient struct {
host string
timeout time.Duration
}
func NewDemoClient(host string) *DemoClient{
return &DemoClient{
host: host,
timeout: DefaultTimeout,
}
}
func NewDemoClientWithTimeout(host string, timeout time.Duration) *DemoClient{
return &DemoClient{
host: host,
timeout: timeout,
}
}
这种方式十分直观,但是扩展性实在太差,如果多几个参数,比如加个maxretry参数
package client
import "time"
const (
DefaultTimeout = 1 * time.Second
DefaultMaxRetry = 3
)
type DemoClient struct {
host string
timeout time.Duration
maxRetry int
}
那难道我再加:
NewDemoClientWithMaxRetry()
NewDemoClientWithTimeoutAndMaxRetry()
多几个参数还不写疯了。要写排列组合个工厂方法。omg。太不geek了。
提供默认Option参数的工厂
优雅很多地,我们可以让工厂方法接受一个Option结构体,并提供默认的Option。
package client
import "time"
const (
DefaultTimeout = 1 * time.Second
DefaultMaxRetry = 3
)
type DemoClient struct {
host string
timeout time.Duration
maxRetry int
}
type Options struct {
Timeout time.Duration
MaxRetry int
}
func NewDefaultOptions() Options {
return Options{
Timeout: DefaultTimeout,
MaxRetry: DefaultMaxRetry,
}
}
func NewDemoClient(host string, opt Options) *DemoClient {
return &DemoClient{
host: host,
timeout: opt.Timeout,
maxRetry: opt.MaxRetry,
}
}
这样构造一个Client的代码就长成这样了:
opt := NewDefaultOptions()
opt.Timeout = 3 * time.Second
client := NewDemoClient("127.0.0.1:8888", opt)
或者也可以不提供DefaultOption,0值如果无意义直接赋值为默认值,如果0值有意义,Option中就改成指针:
package client
import "time"
const (
DefaultTimeout = 1 * time.Second
DefaultMaxRetry = 3
)
type DemoClient struct {
host string
timeout time.Duration
maxRetry int
}
type Options struct {
Timeout time.Duration
MaxRetry *int
}
func NewDemoClient(host string, opt Options) *DemoClient {
client := &DemoClient{
host: host,
timeout: DefaultTimeout,
maxRetry: DefaultMaxRetry,
}
if opt.Timeout != 0 {
client.timeout = opt.Timeout
}
if opt.MaxRetry != nil {
client.maxRetry = *opt.MaxRetry
}
return client
}
这里把Options.MaxRetry从int改成了*int是应为0值是有意义的,代表不重试。为了能让默认是有重试的,得要让用户显式地指定重试次数,赋值一个指针。
这样,很简单的就能创建一个默认的client。
client := NewDemoClient("127.0.0.1:8888", Options{})
这种方法其实已经蛮简单,扩展性很好,满足大部分需求了。如果需要加其他有默认值的参数,只需要往Options里加就行,还能兼容以前的代码。唯一一点小缺点就是还是要创建Options,还是麻烦了一点。
如果想要更加geek一点,那我们可以整上选项模式
选项模式
以下代码通过选项模式实现上述功能:
package client
import (
"time"
)
const (
DefaultTimeout = 1 * time.Second
DefaultMaxRetry = 3
)
type DemoClient struct {
host string
timeout time.Duration
maxRetry int
}
type Option func(opt *Options)
type Options struct {
Timeout time.Duration
MaxRetry int
}
var defaultOption = Options{
Timeout: DefaultTimeout,
MaxRetry: DefaultMaxRetry,
}
func WithTimeout(timeout time.Duration) Option {
return func(opt *Options) {
opt.Timeout = timeout
}
}
func WithMaxRetry(maxRetry int) Option {
return func(opt *Options) {
opt.MaxRetry = maxRetry
}
}
func NewDemoClient(host string, opts ...Option) *DemoClient {
opt := defaultOption
for _, o := range opts {
o(&opt)
}
return &DemoClient{
host: host,
timeout: opt.Timeout,
maxRetry: opt.MaxRetry,
}
}
在上面的代码中,我们定义了Option函数类型,然后利用Go语言的闭包特性,提供工厂方法来返回按需修改Options各字段的Option函数。在 NewDemoClient中,先用defaultOption初始化默认的Options,然后通过回调
for _, o := range opts {
o(&opt)
}
修改Options内的字段。最后创建出对象。
这里通过利用可变参数列表的特性,同样是构建默认对象,只需要:
client := NewDemoClient("127.0.0.1:8888")
比之前的做法少了个Option{}。
带参数时:
client := NewDemoClient("127.0.0.1:8888", WithMaxRetry(3), WithTimeout(3 * time.Second))
并且这种做法使我们实现WithXXXX时能利用闭包的特性实现更大程度的灵活性。比如
// 在around上下浮动5%的超时时间
func WithInaccurateTimeout(around time.Duration) Option {
return func(opt *Options) {
rate := float64(95 + rand.Intn(11)) / 100
opt.Timeout = time.Duration(float64(around) * rate)
}
}
在这个示例中,其实可以直接往Option里传Client:
package client
import (
"time"
)
const (
DefaultTimeout = 1 * time.Second
DefaultMaxRetry = 3
)
type DemoClient struct {
host string
timeout time.Duration
maxRetry int
}
type Option func(opt *DemoClient)
func WithTimeout(timeout time.Duration) Option {
return func(opt *DemoClient) {
opt.timeout = timeout
}
}
func WithMaxRetry(maxRetry int) Option {
return func(opt *DemoClient) {
opt.maxRetry = maxRetry
}
}
func NewDemoClient(host string, opts ...Option) *DemoClient {
client := &DemoClient{
host: host,
timeout: DefaultTimeout,
maxRetry: DefaultMaxRetry,
}
for _, o := range opts {
o(client)
}
return client
}
但不是所有时候都可以这么简化,比如有 只在构造时有效,不需要存到对象的字段中的参数 的时候。
结语
选项模式有很多优点,例如:支持传递多个参数,并且在参数发生变化时保持兼容性;支持任意顺序传递参数;支持默认值;方便扩展;通过 WithXXX 的函数命名,可以使参数意义更加明确,等等。
但缺点是,为了实现选项模式,我们显著地增加了很多代码,实际开发中,要根据场景选择是否使用选项模式。
这里只是示例了工厂函数怎么使用选项模式,通常意义上的函数也可以使用,比如 grpc 中的 rpc 方法就是采用选项模式设计的。
选项模式通常适用于以下场景:
- 结构体参数很多,创建结构体时,我们期望创建一个携带默认值的结构体变量,并选择性修改其中一些参数的值。
- 结构体参数经常变动,考虑以后的扩展,变动时我们又不想修改创建实例的函数。
如果结构体参数比较少,可以慎重考虑要不要采用选项模式,也许直接用带Option的工厂就足够了。