函数选项模式
函数选项模式是一种在Go语言中常用的设计模式,它允许函数调用者通过一系列可选参数来配置对象。这种模式特别适用于初始化时需要大量配置参数的情况,它避免了长参数列表的问题,并提供了一种更清晰、更灵活的方式来设置对象的状态。
优点:
- 减少参数列表:避免函数参数过多,提高代码的可读性。
- 提供默认值:可以为对象提供一组合理的默认值。
- 灵活性:调用者可以只提供他们关心的配置项。
- 可扩展性:可以轻松添加新的配置选项,而不影响现有代码。
引入
一般我们初始化函数的时候:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| type Server struct { Addr string Port int ReadTimeout time.Duration WriteTimeout time.Duration Timeout time.Duration }
func NewServer(addr string, port int, readTimeout, writeTimeout, timeout time.Duration) *Server { return &Server{ Addr: addr, Port: port, ReadTimeout: readTimeout, WriteTimeout: writeTimeout, Timeout: timeout, } }
func main() { s := NewServer("localhost", 8080, 2*time.Second, 2*time.Second, 2*time.Second) fmt.Println(s) }
|
这样子操作有几个缺点:
NewServer
的参数过多,会导致传参的时候过于抽象
NewServer
并不会提供默认值的功能
当我们换一种写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| type Server struct { Addr string Port string ReadTimeout time.Duration WriteTimeout time.Duration Timeout time.Duration }
type Option func(*Server)
func WithAddr(addr string) Option { return func(s *Server) { s.Addr = addr } }
func WithPort(port string) Option { return func(s *Server) { s.Port = port } }
func NewServer(options ...Option) *Server { srv := &Server{ Addr: "localhost", Port: "8080", ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, Timeout: 10 * time.Second, }
for _, opt := range options { opt(srv) }
return srv }
func main() { srv := NewServer(WithAddr("192.168.0.1"), WithPort("9090")) fmt.Println(srv) }
|
在上面的例子中,我们使用 Option
来填充对象的属性,如果不填充也就是直接返回默认值
1 2 3 4 5
| func WithPort(port string) Option { return func(s *Server) { s.Port = port } }
|
因为这里传入的是指针,因此这里的 s
可以被修改
Zap
大名鼎鼎的 zap
就是使用了这种设计模式:
Zap 的核心主要是为了生成一个 Logger
,因此这里的 函数选项模式应该根据Logger
来开展
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| type Logger struct { core zapcore.Core
development bool addCaller bool onPanic zapcore.CheckWriteHook onFatal zapcore.CheckWriteHook
name string errorOutput zapcore.WriteSyncer
addStack zapcore.LevelEnabler
callerSkip int
clock zapcore.Clock }
func New(core zapcore.Core, options ...Option) *Logger { if core == nil { return NewNop() } log := &Logger{ core: core, errorOutput: zapcore.Lock(os.Stderr), addStack: zapcore.FatalLevel + 1, clock: zapcore.DefaultClock, } return log.WithOptions(options...) }
func (log *Logger) WithOptions(opts ...Option) *Logger { c := log.clone() for _, opt := range opts { opt.apply(c) } return c }
|
下面就看他函数是怎么设计的:
1 2 3 4 5 6 7 8 9 10 11
| type Option interface { apply(*Logger) }
type optionFunc func(*Logger)
func (f optionFunc) apply(log *Logger) { f(log) }
|
跟刚刚学的设计模式有所不同,不如说更进一步:
Option interface
这里将 Option
设计成为了 Interface
能够保证在函数设计模式中可以有更加灵活的配置模式
- 用
optionFunc
来满足函数实现,函数实现了这个接口就能够使用这个接口
- Option 接口:定义了一个
apply
方法,任何实现了这个接口的类型都可以作为配置选项。
- optionFunc 类型:这是一个适配器,它允许普通的函数满足
Option
接口。
最后就是装配形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func WrapCore(f func(zapcore.Core) zapcore.Core) Option { return optionFunc(func(log *Logger) { log.core = f(log.core) }) }
func Fields(fs ...Field) Option { return optionFunc(func(log *Logger) { log.core = log.core.With(fs) }) }
func ErrorOutput(w zapcore.WriteSyncer) Option { return optionFunc(func(log *Logger) { log.errorOutput = w }) }
func Development() Option { return optionFunc(func(log *Logger) { log.development = true }) } ......
|
Zap
的写法和我们刚刚之前那一版的写法有少许不同
相比于第一种写法,第二种写法更具有灵活性和特殊性和分类性(好吧这些词都是我自己造的)
在第一种写法中:
1 2 3 4 5 6 7
| type Option func(*Server)
func Withtime(t time.Duration) Option{ return func(s *Server){ s.time = t } }
|
在这里的设计中 ,Withtime
是一个函数,这也就导致这个函数大家都可以使用,而根据工厂模式的设计中,我们更加倾向于下面这种设计模式
1 2 3
| type xx struct{}
func (xx) aa() {}
|
由此可以得出第二种写法
在第二种写法中:
1 2 3 4 5 6 7 8 9
| type Option interface{ apply(*Server) }
type optionFunc func(*Server)
func (f optionFunc) apply(srv *Server){ f(srv) }
|
这里将 Option
设置为 接口后,然后再通过 optionFunc
来具体实现自己的 apply 细节:
1 2 3 4 5
| func Fields(fs ...Field) Option { return optionFunc(func(log *Logger) { log.core = log.core.With(fs) }) }
|
总结
其实不妨想一下:
对于 Interface
和 type xx func
两者最终处理的都是 函数类型
第一种更加依赖类型实现接口来扩展自己的配置,第二种就是比较简单粗暴,只能通过一个又一个的Withxxx
的方式来配置