函数选项模式#

函数选项模式是一种在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)
}

这样子操作有几个缺点:

  1. NewServer 的参数过多,会导致传参的时候过于抽象
  2. 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 // default is WriteThenPanic
onFatal zapcore.CheckWriteHook // default is WriteThenFatal

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
// An Option configures a Logger.
type Option interface {
apply(*Logger)
}

// optionFunc wraps a func so it satisfies the Option interface.
type optionFunc func(*Logger)

func (f optionFunc) apply(log *Logger) {
f(log)
}

跟刚刚学的设计模式有所不同,不如说更进一步:

  1. Option interface 这里将 Option 设计成为了 Interface 能够保证在函数设计模式中可以有更加灵活的配置模式
  2. 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)
})
}

总结#

其实不妨想一下:

对于 Interfacetype xx func 两者最终处理的都是 函数类型

第一种更加依赖类型实现接口来扩展自己的配置,第二种就是比较简单粗暴,只能通过一个又一个的Withxxx的方式来配置