golangの関数のオプション引数を実現する

Gopherなら既にご存知なのだろうけど。
Golangでインスタンスを作成する際に可変長引数を受け付けつつ、拡張を考慮した設計をしたかった。
デフォルト引数を使えればいいけど、Golangにデフォルト引数はないので、色々と探してFunctional Option Patternということを
知ったのでメモ。

簡単な例を示してみてみようと思う。

1
2
3
4
5
6
7
8
9
10
11
package client

type Configs struct {
Port int
Tiemout time.Duration
UserAgent string
}

type Client interface {
Do(req *http.Request) (res *http.Response, err error)
}

のような場合、コンストラクタは普通

1
2
3
4
5
6
7
fucn NewClient(port, timeout, ua) *Configs {
return &Configs{
Port: port,
Tiemout: timeout,
UserAgent: ua,
}
}

とするの一般的。しかし毎回パラメーターを渡す必要がでてくる。
引数なしでインスタンス生成できにない。

そこでコンストラクタを分けるということも考えられる。

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
fucn NewClient() *Configs {
return &Config{
Port: default_port,
Tiemout: default_timeout,
UserAgent: default_ua,
}
}

fucn NewClientWithOption(port, timeout, ua) *Config s {
return &Configs{
Port: port,
Tiemout: timeout,
UserAgent: ua,
}
}

func NewClientWithTPort(port) *Configs {
return &Configs{
Port: port,
Tiemout: default_timeout,
UserAgent: default_ua,
}
}

func NewClientWithTimenout(timeout) *Configs {
return &Configs{
Port: default_port,
Tiemout: timeout,
UserAgent: default_ua,
}
}

func NewClientWithTPort(ua) *Configs {
return &Configs{
Port: default_port,
Tiemout: default_timeout,
UserAgent: ua,
}
}

オプションが増えることで煩雑になる。
他にはあらかじめ構造体を作って渡すという手法。
こちらは明示的で拡張もしやすい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type clientConfigs struct {
Port int
}

func NewClient(configs clientConfigs) *Configs {
return &Config{
Port: configs.port,
Tiemout: configs.timeout,
UserAgent: default_ua,
}
}

// optionが不要な場合
conig := client.NewClient({})

一般的なやり方としてはこうなりそう。しかし、オプションを渡す必要な場合でも
構造体を渡す必要がある。

そこでFunctional Option Patternを試す。

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 Configs struct {
Port int
Tiemout time.Duration
UserAgent string
}

type Option func(*Configs)

func Port(port int) CoOptionnfig{
return func(args *Configs) {
args.Port = port
}
}

func Timeout(time time.Duration) Option{
return func(args *Configs) {
args.Tiemout = time
}
}

func UserAgent(ua string) Option{
return func(args *Configs) {
args.UserAgent = ua
}
}

func New(options ...Option) *Configs {
args := &Configs {
Port: default_port,
Tiemout: default_timeout,
UserAgent: default_ua,
}

for _, option := range options {
option(args)
}

...
}

// Usage
conig := client.NewClient(client.Port(port), client.UserAgent(ua))

オプションを設定する関数を作成し、コンストラクタに可変長引数として渡す。
オプション設定関数はいわゆるクロージャで、設定値を受け取り、オプションを設定するだけになる。
これで、可変長な引数で拡張性のあるオプション設計ができるようになりました。
勉強になりました。

参考ページ

Self-referential functions and the design of options
Functional options for friendly APIs

Comments