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
评论