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)

问题:

  1. 调用方想使用 NewServer 默认行为,为什么不得不传入一个 nil ?
  2. 调用方持有 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))

好处:

  1. Sensible defaults
  2. Highly configurable
  3. Easy to maintain
  4. Self documenting
  5. Safe for newcomers
  6. No need nil or an empty value

Referances

  1. Self-referential functions and the design of options
  2. Functional options for friendly APIs
  3. One example