目录

Golang 中的 Functional Options 模式和 Builder 模式

一、概述

今天我们来看 Golang 中的 Functional Options 模式Builder 模式

一、如何实例化/初始化一个对象

我们从最简单的版本开始,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type Server struct {
	Port     int
	Protocol string
}

func NewServer(port int, protocol string) *Server {
	return &Server{
		Port:     port,
		Protocol: protocol,
	}
}

这种写法应该是多数人信手拈来的版本,对于一个简单的对象初始化场景而言,简洁且优雅,没毛病。

但是我们实际写代码的时候,遇到的需要“配置”的对象往往会更加复杂,比如这样:

1
2
3
4
5
6
7
type DBConnection struct {
	Host     string
	Port     int
	User     string
	Password string
	DBName   string
}

这里有5个配置项,我相信你在调用相应的 NewDBConnection() 函数时已经会开始嫌弃,因为你需要知道5个参数的顺序。我们再泛化下这个场景,如果你需要初始化的那个对象有20个配置项呢?如果你只想设置其中的某几个配置项,其他的都想用默认值呢?你会发现当前这种初始化方式开始变得捉襟见肘。

三、Functional Options 模式

我们尝试重构上面的 Server 配置代码,写成这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Option struct {
	Port     int
	Protocol string
}

type Server struct {
	Option
}

func NewServer(option Option) *Server {
	return &Server{option}
}

这时候你会发现至少在 NewServer() 函数中不需要传递很多的参数了,它们都被封装在了 Option 对象中。但是提供一个完整的 Option 依旧不是一件优雅的事情。(请不要局限于当前的2个配置项,万一是20呢?)

我们继续重构,让每一个配置项的设置都独立成一个函数:

 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
type Option struct {
	Port     int
	Protocol string
}

type Server struct {
	Option
}

type ServerOption func(*Option)

func WithPort(port int) ServerOption {
	return func(o *Option) {
		o.Port = port
	}
}

func WithProtocol(protocol string) ServerOption {
	return func(o *Option) {
		o.Protocol = protocol
	}
}

func NewServer(opts ...ServerOption) *Server {
	o := &Option{}
	for _, opt := range opts {
		opt(o)
	}
	return &Server{*o}
}

这时候调用 NewServer() 的代码就可以这样写:

1
2
3
4
5
6
7
8
func main() {
	server := NewServer(
		WithPort(8080),
		WithProtocol("http"),
	)

	fmt.Printf("Server is running on port %d with protocol %s\n", server.Port, server.Protocol)
}

这时候你就只需要在 NewServer() 函数中“无脑”添加 WithXxx() 函数了,这些函数可读性很好,这时候你配置 Server 会变得轻松愉悦很多。对了,NewServer() 函数里完全可以添加很多的默认行为,比如 Port 默认值设置为80,这样调用的时候如果“不想修改默认端口”,就可以忽略这个配置项了。

四、Builder 模式

还有一种和 Functional Options 模式类似的配置对象的方法叫做 Builder 模式,我们来看这样一个例子:

 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
type Option struct {
	Port     int
	Protocol string
}

type Server struct {
	Option
}

type ServerBuilder struct {
	option Option
}

func NewServerBuilder() *ServerBuilder {
	return &ServerBuilder{}
}

func (b *ServerBuilder) WithPort(port int) *ServerBuilder {
	b.option.Port = port
	return b
}

func (b *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {
	b.option.Protocol = protocol
	return b
}

func (b *ServerBuilder) Build() *Server {
	return &Server{b.option}
}

这时候创建和初始化 Server 的代码就变成了这样:

1
2
3
4
5
6
7
8
9
func main() {
	builder := NewServerBuilder()
	server := builder.
		WithPort(8080).
		WithProtocol("http").
		Build()

	fmt.Printf("Server is running on port %d with protocol %s\n", server.Port, server.Protocol)
}

在 Builder 模式中,我们创建一个 Builder 对象,然后通过调用一系列的方法来配置这个对象。最后,我们调用一个特殊的方法(通常叫做 Build 或者 Create )来获取最终的对象。

五、总结

再来看一次这段代码:

1
2
3
4
	server := NewServer(
		WithPort(8080),
		WithProtocol("http"),
	)

和这段代码:

1
2
3
4
5
	builder := NewServerBuilder()
	server := builder.
		WithPort(8080).
		WithProtocol("http").
		Build()

相比,你更喜欢哪种写法?孰优孰劣,智者见智,我就不下结论了。

总之,Functional Options 模式和 Builder 模式从:

  • 调用者视角来看:两者都提供了一种清晰、易读的方式来创建和配置对象。调用者可以清楚地看到每个选项的名称和值,而不需要记住参数的顺序或者提供所有的参数。
  • 实现者视角来看:两者都允许在不修改已有代码的情况下添加新的配置选项,这有助于代码的维护和扩展。

(关注不迷路,我的个人微信公众号:“胡说云原生”)

(关注不迷路,我的个人微信公众号:“胡说云原生”)

(关注不迷路,我的个人微信公众号:“胡说云原生”)