Functional options for friendly APIs

June 14, 2020

在一些框架代码中,比较常见的封装配置的方法是使用WithXXX()的API形态对外暴露接口,而非传统的Config配置数据结构。本文解释了配置API的集中方案。

一个server package的列子

[text]
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
}

Server暴露出来的API非常简单清晰,但是毫无扩展性。

功能扩展

最简单直接的方法,可能就是增加参数了。

[text]
func NewServer(addr string,
    clientTimeout time.Duration, // 超时时间
    maxconns int, // 最大连接数
    maxconcurrent int, // 最大并发
    cert *tls.Cert) // 安全tls

带来的问题:

  1. 无法向前兼容的问题
  2. 默认值怎么填?如果我们不关心超时,clientTimeout要填什么?

可以根据使用场景,提供不同的constructor,但是这种方法过于笨重了。

[text]
// with timeout
NewServerWithTimeout(addr string, timeout time.Duration) (*Server, error)

// secure server
NewTlsServer(addr string, cert *tls.Cert) (*Server, error)

可配置

将配置单独抽离成Config。

[text]
type Config struct {
    Cert *tls.Cert
    Timeout time.Duration
}

func NewServer(addr string, config Config) *Server

优点:

  1. 向前兼容,增加参数对老版本兼容
  2. 文档化更方便,在Config中针对每项配置说明即可。

但是还是没有解决默认值的问题,很多时候大部分参数用户是不想去理解的。

"I just want a sever, I don't want to have to think about it"

为了避免默认值带来影响,用户在调用的时候对不需要的参数不填默认值。

[text]
func NewServer(addr string, config ...Config) (*Server, error)

func main() {
    svr, _ = NewServer("localhost") // default
    srv2, _ := NewServer("localhost", Config{
        Timeout: 300 * time.Second,
        MaxConns: 10,
    })
}

Functional Options

另一种方法就是将配置拆分成多份,以可变参数的方式传入settler:

[text]
func NewServer(addr string, options ...func(*Server)) (*Server, error) {
    l, _ := net.Listen("tcp", addr)
    srv := Server{listener: l}

    for _, option := range(options) {
        options(&srv)
    }
    return &srv, nil
}

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

    // timeout setter
    timeout := func(srv *Server){
        srv.timeout = 60 * time.Second
    }
    // tls settter
    tls := func(srv *Server){
        config := loadTlsConfig()
        srv.listener = tls.NewListener(srv.listener, &config)
    }
    srv2, _ := NewServer("localhost", timeout, tls)
}

可以将各种setter function封装在package中,不用直接对外暴露config的内部数据结构,而是暴露API。

一个完整的demo如下:

[text]
// Server Package

package Server

import (
    "fmt"
)

type Options struct {
    Name      string
    Age       int32
    Addesss   string
    IdCard    string
    Cellphone string
}

type Option func(*Options)

func NewOptions(opts ...Option) (opt Options) {
    for _, o := range opts {
        o(&opt)
    }
    return
}

func Name(name string) Option {
    return func(o *Options) {
        o.Name = name
    }
}

func Age(age int32) Option {
    return func(o *Options) {
        o.Age = age
    }
}

func Address(addr string) Option {
    return func(o *Options) {
        o.Addesss = addr
    }
}

// 用户调用demo
func main() {
    var opt Options
    opt = NewOptions(
        Name("xigang"),
        Age(24),
        Address("Beijing"))
}
See all postsSee all posts