Go CloudのWireによるコンパイル時依存性注入
Compile-time Dependency Injection With Go Cloud’s Wire by Robert van Gent
概要
Goチームは最近、オープンソースプロジェクトのGo Cloudを発表しました。Go Cloudは、オープンクラウド開発のための、ポータブルなクラウドAPI群とツール群を提供します。この記事では、Go Cloudで使われている依存性注入ツールであるWireについて詳しく説明します。
Wireはどんな問題を解決するのか
依存性注入は、コンポーネントが動作するために必要なすべての依存関係を明示的に与えることで、柔軟で疎結合なコードを生み出すための標準的な手法です。Goでは、これはしばしばコンストラクタに依存関係を渡すという形をとります。
// NewUserStoreはcfgとdbを依存関係として使うUserStoreを返す。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
この手法は小規模であればうまく機能しますが、大規模なアプリケーションでは依存関係のグラフが複雑になり、順序に依存するけれども特段面白みのない大きな初期化コードの塊ができてしまいます。特に一部の依存関係が複数回使われる場合、このコードをきれいに分割するのはしばしば困難です。あるサービスの実装を別の実装に置き換えるのは苦痛を伴うことがあります。それは、依存関係グラフを変更して新しい依存関係一式(そのまた依存関係も…)を追加し、使われなくなった古いものを削除する必要があるからです。実際のところ、大きな依存関係グラフを持つアプリケーションで初期化コードに変更を加えるのは、退屈で時間のかかる作業です。
Wireのような依存性注入ツールは、初期化コードの管理を簡素化することを目指しています。サービスとその依存関係をコードあるいは設定として記述すると、Wireはできあがったグラフを処理して、順序や各サービスに何を渡すべきかを解決します。アプリケーションの依存関係を変更するには、関数のシグネチャを変えたり初期化関数を追加したり削除したりするだけでよく、あとは依存関係グラフ全体の初期化コードを生成するという面倒な作業はWireに任せられます。
なぜこれがGo Cloudの一部になっているのか
Go Cloudの目標は、便利なクラウドサービス向けのイディオマティックなGo APIを提供することで、ポータブルなクラウドアプリケーションを書きやすくすることです。例えば、blob.Bucketは、AmazonのS3とGoogle Cloud Storage(GCS)向けの実装を持つストレージAPIを提供しています。blob.Bucketを使って書かれたアプリケーションは、アプリケーションロジックを変更することなく実装を切り替えられます。しかし、初期化コードは本質的にプロバイダ固有のものであり、各プロバイダは異なる依存関係の集合を持っています。
例えば、GCS向けのblob.Bucketを構築するにはgcp.HTTPClientが必要で、それは最終的にgoogle.Credentialsを必要とします。一方、S3向けにそれを構築するにはaws.Configが必要で、それは最終的にAWSの認証情報を必要とします。したがって、アプリケーションを別のblob.Bucket実装を使うように更新するには、まさに先ほど説明したような依存関係グラフへの面倒な更新作業が必要になります。Wireを生み出す原動力となったユースケースは、Go CloudのポータブルなAPIの実装を簡単に切り替えられるようにすることですが、Wireは依存性注入のための汎用ツールでもあります。
すでに同じようなことは行われているのではないか
世の中には数多くの依存性注入フレームワークが存在します。Goでは、UberのdigとFacebookのinjectが、どちらもリフレクションを使って実行時の依存性注入を行っています。Wireは主にJavaのDagger 2に着想を得ており、リフレクションやサービスロケータではなくコード生成を使います。
この手法にはいくつかの利点があると考えています。
- 依存関係グラフが複雑になると、実行時の依存性注入は追跡もデバッグも難しくなることがあります。コード生成を使えば、実行時に実行される初期化コードは、理解もデバッグもしやすい普通のイディオマティックなGoコードになります。介在するフレームワークが「魔法」を行うことで何かが難読化されることもありません。特に、依存関係を忘れるといった問題は、実行時エラーではなくコンパイル時エラーになります。
- サービスロケータとは異なり、サービスを登録するために任意の名前やキーを考え出す必要がありません。Wireはコンポーネントとその依存関係を結びつけるためにGoの型を使います。
- 依存関係の肥大化を避けやすくなります。Wireが生成するコードは必要な依存関係だけをインポートするので、バイナリに未使用のインポートが含まれることはありません。実行時の依存性注入では、実行時になるまで未使用の依存関係を識別できません。
- Wireの依存関係グラフは静的に把握できるので、ツール化や可視化の機会が生まれます。
仕組み
Wireには、プロバイダ(providers)とインジェクタ(injectors)という2つの基本概念があります。
プロバイダ は、依存関係が与えられたときに値を「提供する」普通のGo関数であり、依存関係は単に関数のパラメータとして記述されます。3つのプロバイダを定義するサンプルコードを見てみましょう。
// NewUserStoreは上で見たのと同じ関数で、UserStoreのプロバイダであり、*Configと*mysql.DBに依存する。
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfigは*Configのプロバイダで、依存関係を持たない。
func NewDefaultConfig() *Config {...}
// NewDBは接続情報に基づく*mysql.DBのプロバイダである。
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}
よく一緒に使われるプロバイダは、ProviderSetにまとめられます。例えば、*UserStoreを作成するときにデフォルトの*Configを使うのはよくあることなので、NewUserStoreとNewDefaultConfigを1つのProviderSetにまとめられます。
var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)
インジェクタ は、依存関係の順序でプロバイダを呼び出す、生成される関数です。必要な入力を引数として含むインジェクタのシグネチャを書き、最終的な結果を構築するのに必要なプロバイダやプロバイダセットのリストとともにwire.Buildの呼び出しを挿入します。
func initUserStore() (*UserStore, error) {
// NewDBは*ConnectionInfoを必要とするが、それを与えていないのでエラーになる。
wire.Build(UserStoreSet, NewDB)
return nil, nil // これらの戻り値は無視される。
}
ここでgo generateを実行してwireを動かしてみます。
$ go generate
wire.go:2:10: inject initUserStore: no provider found for ConnectionInfo (required by provider of *mysql.DB)
wire: generate failed
おっと! ConnectionInfoを含めておらず、それをどう構築するかもWireに伝えていませんでした。Wireは親切にも行番号と関係する型を教えてくれます。wire.Buildにそのためのプロバイダを追加するか、あるいは引数として追加するかのどちらかができます。
func initUserStore(info ConnectionInfo) (*UserStore, error) {
wire.Build(UserStoreSet, NewDB)
return nil, nil // これらの戻り値は無視される。
}
これでgo generateは生成されたコードを含む新しいファイルを作成します。
// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
func initUserStore(info ConnectionInfo) (*UserStore, error) {
defaultConfig := NewDefaultConfig()
db, err := NewDB(info)
if err != nil {
return nil, err
}
userStore, err := NewUserStore(defaultConfig, db)
if err != nil {
return nil, err
}
return userStore, nil
}
インジェクタでない宣言はすべて生成されたファイルにコピーされます。実行時にWireへの依存関係は一切ありません。書かれたコードはすべてただの普通のGoコードです。
ご覧の通り、出力結果は開発者自身が書くコードに非常に近いものになっています。今回はわずか3つのコンポーネントからなる単純な例だったので、初期化関数を手で書いてもそれほど苦痛ではなかったでしょう。しかし、より複雑な依存関係グラフを持つコンポーネントやアプリケーションでは、Wireは多くの手作業の手間を省いてくれます。
参加する方法、さらに詳しく知るには
Wireのreadmeには、Wireの使い方やより高度な機能について詳しく書かれています。また、簡単なアプリケーションでWireを使う流れを説明するチュートリアルもあります。
Wireを使ってみた感想はどんなものでも歓迎します! Wireの開発はGitHub上で行われているので、改善してほしい点があればイシューを立てて教えてください。プロジェクトに関する最新情報や議論については、Go Cloudのメーリングリストに参加してください。
Go CloudのWireについて学ぶために時間を割いていただき、ありがとうございました。ポータブルなクラウドアプリケーションを構築する開発者にとって、Goを第一の選択肢となる言語にするために、皆さんと一緒に取り組めることを楽しみにしています。
By Robert van Gent