• 隐藏侧边栏
  • 展开分类目录
  • 关注微信公众号
  • 我的GitHub
  • QQ:1753970025
Chen Jiehua

Functional options for friendly APIs 

函数式选项模式对于设计友好的API有着重要的作用,它将会影响到你的API后期的扩展以及往前的兼容问题。

一个例子

假如我们要写一个简单的server,我们可以直接这么定义:

type Server struct {
    listener net.Listener
}

func NewServer(addr string) *Server {
    lis, _ := net.Listen("tcp", addr)
    srv := &Server{
        listener: lis
    }
    go srv.Run()
    return srv
}

我们直接用将 addr 作为参数初始化 Server,很简单的实现,同时也很容易使用。

增加功能

随着我们第一个版本发布,可能会开始涌入新的需求:

  • 支持TLS?
  • 限制最大连接数?
  • 请求超时?
  • ……

于是,我们需要修改API加上这些新的需求:

func NewServer(addr string, timeout time.Duration, maxConns int, cert *tls.Cert)

然而,问题便来了:

  • 老用户需要修改代码才能适配我们新的接口;
  • 新用户容易对接口的参数感到迷茫,哪些使必须的,哪些使可选的;
  • 我可能只想开一个简单的server,与我无关的参数太多了;
  • ……

于是,我们可能会换一种方式来定义我们的API:

NewServer(addr string)
NewTLSServer(addr string, cert *tls.Cert)
NewServerWithTimeout(addr string, timeout time.Duration)
NewTLSServerWithTimeout(addr string, cert *tls.Cert, timeout time.Duration)

然而,随着参数增多,我们需要定义的API会逐渐变多,而且不一定保证满足用户需求。

Config Struct

所以我们改用另一种方式来初始化Server,定义一个 Config 结构:

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

func NewServer(addr string, config Config) *Server

这么做有一些好处:

  • 新的参数选项可以很容易地增加,而API可以保持不变;
  • 这种方式也可以更好地进行文档化(不需要为每个API都写文档,只在Config中说明即可);

不过,这种模式仍然不是最佳的,很多时候,用户使用API都希望默认行为就能很好的运行。

而Config这种模式反而潜在着让用户使用零值的问题(Go中未赋值,则默认使零值),并可能会导致问题。

用户可能会更加困扰,Config设置零值、Nil(如果是指针)、或者其它值会有什么不同的效果。

我们再一次改变Config,用可变参数的形式:

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

func main(){
    srv := NewServer("localhost") // defaults
    srv2 := NewServer("localhost", Config{Timeout: 300*time.Second})   // timeout after 5 minutes
}

情况有所好转,如果不想配置Config,用户也不需要给Config传入一个零值

Functional Options

更进一步,我们可以将Config修改得更加通用:

func NewServer(addr string, options ...func(*Server)) *Server

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

    timeout := func(srv *Server){
        srv.timeout = 60 * time.Second
    }
    tls := func(srv *Server){
        // ...
    }
    srv2 := NewServer("localhost", timeout, tls)
}

与上面的不同之处在于:所有的配置参数不再存在于 Config 对象中,而是存在于 Server 自己。

扩展阅读:https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html

于是,我们的API最后就变成了:

func NewServer(addr string, options ...func(*Server))*Server {
    lis, _ := net.Listen("tcp", addr)
    srv := Server{listener: lis}
    for _, option := range options {
         option(&srv)
    }
    return &srv
}

在此,我们提供一个更加完整地例子:

type options struct {
    level    string
    encoding string
}
var defaultOptions = options {
    level: "info",
    encoding: "json",
}

type Option func(*options)

func WithLevevl(level string) Option {
    return func(o *options){
        o.level = level
    }
}

func WithEncoding(encoding string) Option {
    return func(o *options){
        o.encoding = encoding
    }
}

func NewLogger(opts ...Options) *logger {
    opt := defaultOptions
    for _, o := range opts {
        o(&opt)
    }
    logger := log.New(opt.level, opt.encoding)
    return logger
}

最后

总结一下,使用Function Options的方式来设计API,可以使得:

  • Functional options let you write APIs that can grow over time.
  • They enable the default use case to be the simplest.
  • They provide meaningful configuration parameters.
  • Finally they give you access to the entire power of the language to initialize complex values.

在API设计过程中,我们可以不断地问自己:

  • Can this be made simpler ?
  • Is that parameter necessary ?
  • Does the signature of this function make it easy for it to be used safely ?
  • Does the API contain traps or confusing misdirection that will frustrate ?

 

原文:https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis

码字很辛苦,转载请注明来自ChenJiehua《Functional options for friendly APIs》

评论