Functional Options Pattern に次ぐ、オプション引数を実現する方法 #golang
coding Go
Go 言語で関数を呼び出す際に optional な引数を実現したい場合、 Functional Options Pattern (以下:FOP) を使われているでしょうか。
FOP は非常に有用で、特に client 等を初期化する際の数多のオプションを管理するのに便利です。
2014年の記事ですが、 FOP については Rob Pike 氏, Dave Cheney 氏の記事が参考になります。
- Rob Pike: Self-referential functions and the design of options
- Dave Cheney: Functional options for friendly APIs
それから5年経って、いまでも FOP が最善でしょうか。
否、 FOP には1点だけ問題があります。 それは mock (e.g. https://github.com/golang/mock) を用いたテストで再現します。
FOP では Option は function で表されるため、 mock で引数のマッチングを試みると、同じ値を示す Option 同士でも一致しません。 なぜなら、次のコードは false を出力するからです。
package main
import (
"fmt"
"reflect"
)
type conf struct{ a int }
type option func(c *conf)
func OptionA(v int) option {
return func(c *conf) {
c.a = v
}
}
func main() {
fmt.Println(reflect.DeepEqual(OptionA(2), OptionA(2)))
}
FOP: https://play.golang.com/p/kmhgBDlpfbZ
同じ Option の値を表す OptionA(2)
であっても、 reflect.DeepEqual
の結果は false
になります。
この問題を解決するため、新たに Applicable Functional Option Pattern (以下:AFOP) を提案します。
※勝手に命名したので、すでに名前が付いていたらどなたかこっそり教えて下さい。
package main
import (
"fmt"
"reflect"
)
type Option interface {
Apply(*conf)
}
type conf struct{ a int }
type AOption int
func (o AOption) Apply(c *conf) {
c.a = int(o)
}
func OptionA(v int) AOption {
return AOption(v)
}
func main() {
fmt.Println(reflect.DeepEqual(OptionA(12), OptionA(12)))
fmt.Println(OptionA(12) == OptionA(12))
}
AFOP: https://play.golang.org/p/paWrNpmZYN_j
interface で Option
を定義し、実際の値を表す type で実装することで、 FOP とほぼ同じ使い勝手のまま mock 対応が可能です。
これは google.golang.org/api/option
を参考にしています。
c.f. https://github.com/googleapis/google-api-go-client/blob/v0.7.0/option/option.go
もう少し具体的な例はこちら: https://play.golang.org/p/rFoYqiYicB9
今後、 optional な引数を取りたい時は AFOP で。
追記
FOP と AFOP には、オプションを第一級関数として表現するか、インターフェースを用いて表現するかといった違いがありますが、これらに本質的な違いはなく AFOP も Functional Options Pattern に含まれるという話もあるようです。
恐らく、「interface として実装された Functional Options」と表現すると正しそうです。