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 で同じオプションを使用できるという点で再利用可能です。
- コンパイル時にエラーが発生するという点で型安全です。間違ったオプションを渡すと、エラーが発生します。