Go API のための再利用可能で型安全なオプションの実装方法
Reusable and type-safe options for Go APIs by Derek Chiang on
原著者の許諾を得て翻訳・掲載しています。
背景
本記事では、Rob Pike 氏と Dave Cheney 氏により記述された「Functional Option Pattern」の拡張について説明したいと思います。このパターンに慣れていない人は、まず彼らの記事を読むことをおすすめします。
問題
このパターンの限界を見るために、etcd v3 client について考えてみます。特に、key-value の値を取得したり登録したりするための API である KV
インターフェースに着目してください。例えば、Get
の API はこのような形です:
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)
ここで、opts
は functional options のリストです。この API を呼び出すには、以下のように書きます:
resp, err := kvc.Get(ctx, "sample_key", WithPrefix(), WithRev(sample_rev))
ここでは WithPrefix
と WithRev
という2つのオプションを API に渡しています。ここで注意していただきたいのは、この2つのオプションは関数だということです。したがって、任意のオプションを取ることができます。
このアプローチの問題ですが、正しいかどうかにかかわらず、API に対して OpOption
により任意のオプションを渡すことができる、ということです。例えば、 WithLease オプションが Put
にのみ利用可能なものでも、Get
に渡すことができます。
したがって、そのオプションは型安全ではないと言えます。ちなみに、間違ったオプションを渡した場合、ランタイムでのみ検出可能で、コンパイル時には検出できません。
誤った解決策
API(GET や PUT)ごとに違った引数の型を定義することにより解決したくなります。例えば、Get
を受け付ける GetOption
、Put
のみを受け付けられる PutOption
を定義する、などです。
このアプローチの問題点は、それぞれの API が同じような実装をもってしまうことです。Go 言語が関数のオーバロードをサポートしていないので、以下のように、各APIに一つ、同じオプションを持った別々の関数を定義する必要があります。以下のような形です:
func WithPrefixForGet() GetOption { ... }
func WithPrefixForDelete() DeleteOption { ... }
func WithPrefixForWatch() WatchOption { ... }
これは、開発者にとってもユーザーにとっても明らかに理想的ではありません。各 API に対してひとつのパッケージとしてオプションを定義したくなるかもしれませんが、より扱いにくくなるだけです。
解決策
ここに例を載せました。簡単にするために、GET
と DELETE
という2つの API と WithPrefix
と WithRev
という2つのオプションのみを扱うことにします。
まず最初に、API を定義します:
func Get(key string, ops ...GetOption)
func Delete(key string, ops ...DeleteOption)
そして、各 API に一つ、インターフェースを定義します:
type GetOption interface {
SetGetOption(*getOptions)
}
type DeleteOption interface {
SetDeleteOption(*deleteOptions)
}
最後に、各オプションに対して1つの関数を定義します:
// WithPrefix は Get と Delete で使えます
func WithPrefix() interface {
GetOption
DeleteOption
} {
// 上記リンクにある実装を参照してください
}
// WithRev は Get でのみ使えます
func WithRev(rev int64) interface {
GetOption
} {
// 上記リンクにある実装を参照してください
}
ここで注意すべきことは、関数が *Option
インターフェースが埋め込まれた無名のインターフェースを返していることです。これは、型を見ることで、どの API を使用できるかをすぐに知ることができるという点で、コード自身がドキュメントの役割を果たしている(また godoc
フレンドリーでもある)という利点があります。
次に、オプションが実際に再利用可能で、型安全であるかどうかを見てみましょう。WithPrefix()
が GetOption
と DeleteOption
を実装しているので、以下のコードは問題なく動作します:
Get("sample_key", WithPrefix())
Delete("sample_key", WithPrefix())
しかし、Delete
にWithRev
を渡した場合、コンパイルエラーが起きます:
Delete("sample_key", WithRev(1))
./main.go:88:30: cannot use WithRev(1) (type interface { SetGetOption(*getOptions) }) as type DeleteOption in argument to Delete:
interface { SetGetOption(*getOptions) } does not implement DeleteOption (missing SetDeleteOption method)
これは、WithRev
が DeleteOption
ではないということを教えてくれています!
まとめ
以下に、オプションを定義するためのパターンをまとめました:
- 複数の API で同じオプションを使用できるという点で再利用可能です。
- コンパイル時にエラーが発生するという点で型安全です。間違ったオプションを渡すと、エラーが発生します。