如何通过“功能选项”来设计友好的 API

Page content

场景假设

你作为公司的顶梁柱,要编写一个服务器组件

阶段一:上手就撸版本

type Server struct {
  listener net.Listener
}

func (s *Server) Addr() net.Addr

func (s *Server) Shutdown()

func NewServer(addr string) (*Server, error) {
  l, err := net.Listen("tcp", addr)
  if err != nil {
    return nil, err
  }
  srv := Server{listener: l}
  go srv.run()
  return &srv, nil
}

优点

  • 三分钟即可上线,啥都不管,无脑 NewServer() 就完事了

缺点

  • 一切皆为默认,想要定制是不可能的

阶段二:巨多参数版本

新的需求来了:

  • 我要限制最大连接数
  • 我要设置超时时间
  • 我要设置 tls 证书
  • 我要我还要……

那就增加点参数,整个可配置版吧

// NewServer returns a new Server listening on addr.
// clientTimeout defines the maximum length of an idle
// connection, or forever if not provided.
// maxconns limits the number of concurrent connections.
// maxconcurrent limits the number of concurrent
// connections from a single IP address.
// cert is the TLS certificate for the connection.
func NewServer(addr string, clientTimeout time.Duration, 
                maxconns, maxconcurrent int, cert *tls.Cert) {
    ...
}

优点

  • 可配置了,想配啥就配啥

缺点

  • 啥都可配了,但是我不想啥都自己配,对于不想配置的参数我该传什么?
  • 最大连接数我不想管,我想使用默认值,那我传 0 可以吗?传 0 不会最大连接数就是 0 了吧!oh my god!

阶段三:巨多函数版本

// NewServer returns a Server listening on addr.
NewServer(addr string) (*Server, error)

// NewTLSServer returns a secure server listening on addr.
NewTLSServer(addr string, cert *tls.Cert) (*Server, error)

// NewServerWithTimeout returns a Server listening on addr that disconnects idle clients.
NewServerWithTimeout(addr string, timeout time.Duration) (*Server, error)

// NewTLSServerWithTime returns a secure Server listening on addr that disconnects idle clients.
NewTLSServerWithTimeout(addr string, cert *tls.Cert, timeout time.Duration) (*Server, error)

参数自由组合,不同的组合对应不同的函数

优点

  • 避免了阶段二的问题,不想配置的参数咱不选就是

缺点

  • 参数越多,函数越多,越来越难维护

阶段四:配置结构版本

// Config structure is used to configure the Server.
type Config struct {
  // Timeout sets the amount of time before closing
  // idle connections, or forever if not provided.
  Timeout time.Duration

  // The server will accept TLS connections is the
  // certificate provided.
  Cert *tls.Cert
}

func NewServer(addr string, config Config) (*Server, error)

优点

  • 避免了阶段三的问题
  • 添加新的配置项只需要在 Config 结构体增加,NewServer 的定义不会改变

缺点

  • 0 值迷惑:字段的 0 值是调用者有意为之?还是调用方压根没传?
  • 所有选项都不想配置时,仍需要构造一个空的 Config 作为参数

阶段五:指针优化版本

func NewServer(addr string, config *Config) (*Server, error) {...}

func main() {
  src, _ := NewServer("localhost", nil)

  config := Config(Port: 9000)
  srv2, _ := NewServer("localhost", &config)

  config.Port = 9001 // what happens now?
  ...
}

优点

  • 全部使用默认值时,可以传递 nil,而不用构造一个空的 Config

缺点

  • 疑惑1: nil 和空 Config 的区别是什么?
  • 疑惑2: 使用 config 构建好了 server,再修改 config 会发生什么?

阶段六:可变配置版本

func NewServer(addr string, config ...Config) (*Server, error) {...}

func main() {
  srv, _ := NewServer("localhost") // defaults

  // timeout after 5 minutes, 10 clients max
  srv2, _ := NewServer("localhost", Config{
      Timeout:  300 * time.Second,
      MaxConns: 10,
  })
}

优点

  • 消除了阶段五的疑惑,想使用默认值时,什么都不传即可

缺点

  • 原则上我们期望最多一个 config,但是函数的定义是可变的,可传递多个 config,更糟糕的是,这些 config 的值可能是相互矛盾的

阶段七:可变函数版本

func NewServer(addr string, options ...func(*Server)) (*Server, error) {...}

func main() {
  srv, _ := NewServer("localhost")

  timeout := func(srv *Server) {
      srv.timeout = 60 * time.Second
  }

  tls := func(srv *Server) {
      config := loadTLSConfig()
      srv.listener = tls.NewListener(srv.listener, &config)
  }

  // listen securely with a 60 second timeout
  srv2, _ := NewServer("localhost", timeout, tls)
}

优点

  • 消除了阶段六的缺点,生成 server 时,依次调用 func,如果多次设置一个配置项,后面的 func 会覆盖前面的,这合乎情理

文章参考: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis http://47.103.196.252/posts/api%E8%AE%BE%E8%AE%A1%E5%8F%AF%E9%80%89%E7%9A%84%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0