函数项模式(Functional Options Pattern)是一种创造性的设计模式,允许使用接受零个或多个函数作为参数的可变构造函数来构建复杂结构
在没有函数项模式之前,在包初始化加载配置选项的时候,一般有两种做法
编写不同的构造函数
第一种方法是给每种不同的配置选项编写不同的构造函数
比如有配置项 host、port、maxConn、timeout,把配置项抽象为结构体有:
type Config struct {host stringport intmaxConn inttimeout time.Duration}
想要实例化这个结构体,最直接的构造函数为:
func NewConfig(host string, port, maxConn int, timeout time.Duration) *Config {return &Config{host, port, maxConn, timeout}}
调用 NewConfig 函数后,需要传入 host、port、maxConn、timeout 参数,返回一个指针类型的 Config 对象
但是一般来说,host 和 post 是必填的字段,而 maxConn 和 timeout 应该是可选的,所以构造函数需要考虑是否初始化 maxConn 和 timeout 字段
所需要的构造函数有:
// 配置 maxConn 和 timeout func NewConfigWithmaxConnAndTimeout(host string, port, maxConn int, timeout time.Duration) *Config {return &Config{host, port, maxConn, timeout}}// 只配置 maxConnfunc NewConfigWithMaxConn(host string, port, maxConn int) *Config {// timeout 字段默认值为 time.Minutereturn &Config{host, port, maxConn, time.Minute}}// 只配置 timeoutfunc NewConfigWithTimeout(host string, port int, timeout time.Duration) *Config {// maxConn 字段默认值为 100return &Config{host, port, 100, timeout}}// maxConn 和 timeout 都使用默认值func NewConfig(host string, port int) *Config {return &Config{host, port, 100, time.Minute}}
对于不同的配置选项编写不同的构造函数,适用于配置较少且变化很少的场景。如果上面的配置项增加一项,那么所有的构造函数都要改一遍,这种方式可伸缩性很差
使用结构体嵌套
第二种方法是使用一个专门用于配置等要求的结构体
当配置项很多的时候,可以创建一个结构体专门存放不同类型的配置选项
比如:
type AppConfig struct {host stringport intmaxConn inttimeout time.Duration}type LogConfig struct {Level stringFilename stringmaxSize int}type Config struct {appConfig AppConfiglogConfig LogConfig}
把配置项按功能拆分,用一个 Config 结构体包含所有的配置选项,这样构造函数可以这样写:
func NewConfig(appConfig AppConfig, logConfig LogConfig) *Config {return &Config{appConfig, logConfig}}
这种方法有了一定的扩展性,后续有更多的配置选项时,只需要把对应配置项结构体放入 Config 结构体中就可以
但是,要想构造 Config 实例,需要先构造 AppConfig 实例、LogConfig 实例,构造的过程又回到了第一种方法,需要根据不同的配置编写很多的构造函数,而且可伸缩性很差
这种方法适用于配置项很多,变化较少的场景
函数项模式
可以看出来,上面两种方法,扩展性都不是很好,并且不够“优雅”。添加字段会频繁的修改构造函数
使用函数项模式,就可以更灵活的初始化配置选项
使用时,要先定义一个函数类型
type Option func(*Config)
Option 是一个函数类型,接收一个 *Config 的参数
在设计构造函数的时候,只需要
func NewConfig(options ...Option) *Config {cfg := &Config{}for _, ops := range options {ops(cfg)}return cfg}
分析一下这个构造函数,它接收一个可变数量的 Option 类型,返回一个 *Config 类型
options 是一个函数类型的切片,存放要对 cfg 做的各种修改,通过遍历 options,对 cfg 实例做各种选项配置,最后返回初始化完成的 Config 实例指针
要想使用这个构造函数,需要先定义各种返回 Option 类型的选项函数
// 配置 host 和 portfunc WithHostAndPort(host string, port int) Option {return func(cfg *Config) {cfg.host = hostcfg.port = port}}// 配置 maxConnfunc WithMaxConn(maxConn int) Option {return func(cfg *Config) {cfg.maxConn = maxConn}}// 配置 timeoutfunc WithTimeout(timeout time.Duration) Option {return func(cfg *Config) {cfg.timeout = timeout}}
构造函数这样使用:
cfg := NewConfig(WithHostAndPort("localhost", 8088),WithMaxConn(100),WithTaimeout(time.Minute),)
完整示例代码:
package mainimport "time"// Config 配置结构体type Config struct {host stringport intmaxConn inttimeout time.Duration}// 打印配置项func (cfg Config) print() {println("host:", cfg.host)println("port:", cfg.port)println("maxConn:", cfg.maxConn)println("timeout:", cfg.timeout)}type Option func(*Config)func NewConfig(options ...Option) *Config {cfg := &Config{}for _, ops := range options {ops(cfg)}return cfg}// WithHostAndPort 配置 host 和 portfunc WithHostAndPort(host string, port int) Option {return func(cfg *Config) {cfg.host = hostcfg.port = port}}// WithMaxConn 配置 maxConnfunc WithMaxConn(maxConn int) Option {return func(cfg *Config) {cfg.maxConn = maxConn}}// WithTimeout 配置 timeoutfunc WithTimeout(timeout time.Duration) Option {return func(cfg *Config) {cfg.timeout = timeout}}func main() {cfg := NewConfig(WithHostAndPort("localhost", 8088),WithMaxConn(100),WithTimeout(time.Minute),)cfg.print()/*host: localhostport: 8088maxConn: 100timeout: 60000000000*/}
将来要增加配置选项,只需要增加对应的 WithXXX 选项函数即可,对其他配置项没有任何影响
总结
函数项模式提供了一种更灵活和动态的编码方式,在以下场景可以考虑使用函数项模式:
-
参数确实比较复杂,影响调用方使用
-
参数确实有比较清晰明确的默认值
-
为参数的后续拓展考虑