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
	}
}

参考资料