Functional Options Pattern
一个 Server 拥有很多配置选项,创建对象时一部分选项需要强制指定,另一部分选项非强制指定,且有缺省值。
如何设计一个对调用方友好的 NewServer 构造函数 API ?
Configuration Problem
例如 Addr 和 Port 是需要强制指定的选项,其他选项非强制指定,且有缺省值,该如何设计 New Server 构造函数呢?
1
2
3
4
5
6
7
8
|
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
TLS *tls.Config
}
|
我们可以创建一组针对不同情况的构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
|
func NewServer(addr string, port int) (*Server, error) {
//...
}
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
//...
}
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
//...
}
func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
//...
}
|
但是随着 option 数量增加,这种方案很快便会让构造函数的数量骤增,不堪负重。如何改进呢?
Passing a pointer of configuration struct
将非强制性选项提取出一个配置结构。只需要一个构造函数,且不管 option 数量怎么增加,构造函数 API 都无需改变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
type ServerConfig struct {
Protocol string
Timeout time.Duration
Maxconns int
TLS *tls.Config
}
type Server struct {
Addr string
Port int
Conf *ServerConfig
}
func NewServer(addr string, port int, conf *ServerConfig) (*Server, error) {
//...
}
|
调用方:
1
2
3
4
5
6
|
// Using the default configuration
srv1, _ := NewServer("localhost", 9000, nil)
// Using the default configuration and another config object
conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration}
srv2, _ := NewServer("locahost", 9000, &conf)
|
问题:
- 调用方想使用 NewServer 默认行为,为什么不得不传入一个 nil ?
- 调用方持有 ServerConfig 对象指针,当传入 NewServer 之后,将之修改,会发生什么?
Functional Options Pattern
用操作 *Server 的 function 取代用 ServerConfig 结构中的 option 来处理。
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
|
type Option func(*Server)
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConns(maxconns int) Option {
return func(s *Server) {
s.MaxConns = maxconns
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConns: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
return &srv, nil
}
|
调用方:
1
2
3
|
s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))
|
好处:
- Sensible defaults
- Highly configurable
- Easy to maintain
- Self documenting
- Safe for newcomers
- No need nil or an empty value
Referances
- Self-referential functions and the design of options
- Functional options for friendly APIs
- One example