一、概述
今天我们来看 Golang 中的 Functional Options 模式和 Builder 模式。
一、如何实例化/初始化一个对象
我们从最简单的版本开始,如下:
type Server struct {
Port int
Protocol string
}
func NewServer(port int, protocol string) *Server {
return &Server{
Port: port,
Protocol: protocol,
}
}
这种写法应该是多数人信手拈来的版本,对于一个简单的对象初始化场景而言,简洁且优雅,没毛病。
但是我们实际写代码的时候,遇到的需要“配置”的对象往往会更加复杂,比如这样:
type DBConnection struct {
Host string
Port int
User string
Password string
DBName string
}
这里有5个配置项,我相信你在调用相应的 NewDBConnection()
函数时已经会开始嫌弃,因为你需要知道5个参数的顺序。我们再泛化下这个场景,如果你需要初始化的那个对象有20个配置项呢?如果你只想设置其中的某几个配置项,其他的都想用默认值呢?你会发现当前这种初始化方式开始变得捉襟见肘。
三、Functional Options 模式
我们尝试重构上面的 Server 配置代码,写成这样:
type Option struct {
Port int
Protocol string
}
type Server struct {
Option
}
func NewServer(option Option) *Server {
return &Server{option}
}
这时候你会发现至少在 NewServer()
函数中不需要传递很多的参数了,它们都被封装在了 Option
对象中。但是提供一个完整的 Option 依旧不是一件优雅的事情。(请不要局限于当前的2个配置项,万一是20呢?)
我们继续重构,让每一个配置项的设置都独立成一个函数:
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()
的代码就可以这样写:
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 模式,我们来看这样一个例子:
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 的代码就变成了这样:
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 )来获取最终的对象。
五、总结
再来看一次这段代码:
server := NewServer(
WithPort(8080),
WithProtocol("http"),
)
和这段代码:
builder := NewServerBuilder()
server := builder.
WithPort(8080).
WithProtocol("http").
Build()
相比,你更喜欢哪种写法?孰优孰劣,智者见智,我就不下结论了。
总之,Functional Options 模式和 Builder 模式从:
- 调用者视角来看:两者都提供了一种清晰、易读的方式来创建和配置对象。调用者可以清楚地看到每个选项的名称和值,而不需要记住参数的顺序或者提供所有的参数。
- 实现者视角来看:两者都允许在不修改已有代码的情况下添加新的配置选项,这有助于代码的维护和扩展。