Functional Options Pattern に次ぐ、オプション引数を実現する方法 #golang

             

Go 言語で関数を呼び出す際に optional な引数を実現したい場合、 Functional Options Pattern (以下:FOP) を使われているでしょうか。

FOP は非常に有用で、特に client 等を初期化する際の数多のオプションを管理するのに便利です。

2014年の記事ですが、 FOP については Rob Pike 氏, Dave Cheney 氏の記事が参考になります。

それから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 に含まれるという話もあるようです。

[追記] そもそもFunctional Options Patternとは何を指しているのかという話 | GoでFunctional Options Patternを使うとモックで引数の比較ができない問題に対応したい

恐らく、「interface として実装された Functional Options」と表現すると正しそうです。

参考