Go 中的 Options 模式

Functional Options Pattern(函数式选项模式)可用于传递不同选项配置到方法中,而且每次新增选项时,可以不改变接口保持兼容。还可以用来实现类似 Java/C++ 中方法重写的功能~

Go 中结构体没有构造方法,所以有的时候我们会自己提供一个 New 方法。考虑你有某个客户端结构体:

1
2
3
4
5
6
7
type Client struct {
	// ...
}

func New() *Client {
	return &Client{}
}

有一天需要新增一个 Timeout 参数,由于 Go 不支持方法重载,因此我们不得不修改 New 方法传入一个 Timeout 参数, 但是一旦修改了 New 的签名,将会造成不兼容。于是我们只好新增一个工厂方法 NewClientWithTimeout 并且修改旧有的 New 方法,设置它的 Timeout 为一个默认值。

但是下次再需要新增字段,还是很麻烦……

我们可以使用可变参数来解决这一问题。

先定义一个配置结构体:

1
2
3
4
5
type Config struct {
   Timeout time.Duration
   Cluster string
}
type Option func(*Config)

然后对每个配置项提供一个配置函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func WithTimeout(timeout time.Duration) Option {
	return func(config *Config) {
		config.Timeout = timeout
	}
}
func WithCluster(cluster string) Option {
	return func(config *Config) {
		config.Cluster = cluster
	}
}

最后在 New 方法上使用可变参数来接收配置函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func New(opts ...Option) *Client {
	// default config
	config := &Config{Timeout: 20 * time.Millisecond, Cluster: "default"}
	// for each custom options
	for _, opt := range opts {
		opt(config)
	}
	return &Client{
		Timeout: config.Timeout,
		Cluster: config.Cluster,
	}
}

下次再需要新增配置项,只需要新增 WithXxx 配置函数,修改默认 Config 的值即可。调用方可以直接升级版本,如果不传入新的配置函数,则会使用默认配置。

1
2
3
New()
New(WithTimeout(10 * time.Millisecond))
New(WithTimeout(10*time.Millisecond), WithCluster("my-cluster"))

这个模式还可以用来模仿 Java8 的接口默认实现、或者用来模仿 Java 中的子类重写(override)父类方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package foo

// 
type MyInterface interface {
	SayHi() string
}

// 
type base struct {
	//  Go 
	sayHi func() string
}

// 使
func (b *base) SayHi() string {
	return b.sayHi()
}

// 
type Option func(*base)

func SayHi(f func() string) Option {
	return func(b *base) {
		b.sayHi = f
	}
}

// 
func newDefault() *base {
	return &base{
		sayHi: func() string {
			return "Hello, World"
		},
	}
}

// 
func New(opts ...Option) MyInterface {
	b := newDefault()
	for _, opt := range opts {
		opt(b)
	}
	return b
}

使用方:

1
2
3
4
5
6
7
8
9
func ExampleNew() {
	fmt.Println(foo.New().SayHi())
	fmt.Println(foo.New(foo.SayHi(func() string {
		return "你好,世界"
	})).SayHi())
	// Output:
	// Hello, World
	// 
}

这样就可以通过传入函数的形式改变默认的行为,看起来像重写?

Go Streams 仿照 Java8 的流造的轮子》中 stage 接口使用了这一模式。

Reader Echoes

No comments yet

表情

评论提交后需经审核才会显示。