Functional Options Pattern
在初始化一个对象的时候,我们通常会定义一个 NewXXX(serverName stirng, timeout int)
的 New 函数,在参数中传入一些配置项,做一些定制化的配置。
此时如果你想新增一个配置项,就需要修改函数的签名,新增一个参数。但是,NewXXX
函数已经在很多地方使用了,新增一个参数需要修改所有使用到它的地方,这样显然成本太高了。
在 Go 语言的开发范式中有一种 optionFunc
配置模型可以在不修改函数签名的情况下新增配置项,使用时只需要用不同的 WithXXX 函数传递不同的参数,可以随意组合:
obj := NewXXX(
WithTimeout(5 * time.Second),
// WithXXX() ...
)
通过使用这种模式,可以实现高度的可配置化,配置也易于维护、扩展和整洁优雅。
操作流程
1. 定义 options 对象
定义一个 options
结构体汇总所有的配置:
type options struct {
serverName string
timeout time.Duration
// ...
}
2. 定义 optionFunc 类型
optionFunc
定义成一个函数类型,以 options
作为参数。
type optionFunc func(*options)
3. NewXXX 函数框架
关键在于 NewXXX
函数的参数定义,需要定义成【可变参数】,这样才可以动态地增减配置。
func NewXXX(opts ...optionFunc) (*XXX, error) {
// 初始化 options 可以设置一些默认值
opts := &options{
timeout: 5,
}
// 调用配置函数修改配置
for _, opt := opts {
opt(opts)
}
// 使用 options 初始化业务对象
xxx = &XXX{
options: opts,
// ...
}
// 返回配置好的对象
return xxx, nil
}
4. 定义不同属性的配置函数
以配置 timeout
为例:
func WithTimeout(t time.Duration) optionFunc {
return func(opts *options) {
opts.timeout = t
}
}
变化形式:使用 Option 接口
还有一种思想类型的配置模式,会定义一个 Option
接口,并用 funcOption 对象实现接口:
type ClientOption interface {
apply(*client)
}
type funcClientOption struct {
f func(*client)
}
func (fdo *funcClientOption) apply(do *client) {
fdo.f(do)
}
func newFuncClientOption(f func(*client)) *funcClientOption {
return &funcClientOption{
f: f,
}
}
// OptServicerMapper 配置选项,设置 servicer.MapPicker
func OptServicerMapper(sm servicer.MapPicker) ClientOption {
return newFuncClientOption(func(c *client) {
c.mapper = sm
})
}
// OptLogger 可选,配置打印 mysql 访问日志的 logger
func OptLogger(logger logit.Logger) ClientOption {
return newFuncClientOption(func(c *client) {
c.SetLogger(logger)
})
}
// OptObserver 在初始化 client 时,注册执行结果观察器
//
// Deprecated: 换成 OptInterceptor,功能更多
func OptObserver(hks ...ObserverFunc) ClientOption {
return newFuncClientOption(func(c *client) {
for _, fn := range hks {
c.addObserverFunc(fn)
}
})
}
// OptInterceptor 在初始化 client 时,注册拦截器,可以用于替代 OptObserver
func OptInterceptor(its ...*Interceptor) ClientOption {
return newFuncClientOption(func(c *client) {
for _, it := range its {
c.registerInterceptor(it)
}
})
}
func optDefault() ClientOption {
return newFuncClientOption(func(c *client) {
// 优先使用mysql 全局默认 logger
if c.Logger() == nil && DefaultLogger != nil {
c.SetLogger(DefaultLogger)
}
// 默认使用ral 的 日志
if c.Logger() == nil && ral.DefaultRaller != nil {
c.SetLogger(ral.DefaultRaller.WorkLogger())
}
if c.dbPoolFac == nil {
c.dbPoolFac = func(hs HasServicer) Pool {
return newPool(hs, c.connect)
}
}
// 注册日志中间件
if !DisableDefaultLog {
li := &LogInterceptor{}
c.registerInterceptor(li.Interceptor())
}
})
}
func NewClient(serviceName any, opt ...ClientOption) (Client, error) {
c := &client{
servicerName: serviceName,
}
for _, o := range opt {
o.apply(c)
}
optDefault().apply(c)
return c, nil
}
这个比上面的模式理解起来要复杂一些,但底层思想是一致的。
简单场景直接配置业务对象
另外上面定义的 options 也可以直接使用业务对象,例如一个 Server,在初始化时,对它的内部字段做配置。
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
}
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
}
}
WHY?
感觉本质就是一种 Closure 的玩法,将函数词法作用域捕获的闭包变量在适当的时机赋值给实际的对象,核心是这个高阶函数:
func(values) FuncCarryValues {
return func(valuesConsume) {
// ...
valuesConsume.x = values.x
}
}