Goブログ
Go CloudのWireによるコンパイル時依存性注入
概要
Goチームは最近、ポータブルなクラウドAPIとオープンクラウド開発のためのツールを提供するオープンソースプロジェクト発表しました。Go Cloud。この記事では、Go Cloudで使用されている依存性注入ツールであるWireについて詳しく説明します。
Wireはどのような問題を解決するのか?
コンポーネントに必要なすべての依存関係を明示的に提供することで、柔軟で疎結合なコードを作成するための標準的な手法である依存性注入。Goでは、これは多くの場合、コンストラクタに依存関係を渡すという形で実現されます。
// NewUserStore returns a UserStore that uses cfg and db as dependencies.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
この手法は小規模では非常にうまく機能しますが、大規模なアプリケーションでは依存関係のグラフが複雑になり、順序に依存するが、それ以外の点ではあまり興味のない大きな初期化コードブロックが生成されます。特に、一部の依存関係が複数回使用されるため、このコードをきれいに分割するのは難しいことがよくあります。サービスの1つの実装を別のものと置き換えるのは、依存関係グラフ全体に新しい依存関係セット(とその依存関係…)を追加し、使用されていない古い依存関係を削除する必要があるため、困難です。実際、大規模な依存関係グラフを持つアプリケーションの初期化コードを変更することは、面倒で時間がかかります。
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の実装を簡単に交換できるようにすることですが、依存性注入の汎用ツールでもあります。
すでにこれと同じことは行われていないか?
多くの依存性注入フレームワークが存在します。Goの場合、UberのdigとFacebookのinjectはどちらもリフレクションを使用してランタイム依存性注入を行います。Wireは主にJavaのDagger 2から着想を得ており、リフレクションやサービスロケータではなく、コード生成を使用します。
このアプローチにはいくつかの利点があると私たちは考えています
- 依存関係グラフが複雑になると、ランタイム依存性注入は追跡およびデバッグが困難になる可能性があります。コード生成を使用すると、ランタイムで実行される初期化コードは、理解とデバッグが容易な通常の慣習的なGoコードになります。「魔法」を実行する介入フレームワークによって何も曖昧になりません。特に、依存関係を忘れるような問題は、ランタイムエラーではなく、コンパイル時エラーになります。
- サービスロケータとは異なり、サービスを登録するための任意の名前やキーを作成する必要はありません。WireはGo型を使用して、コンポーネントとその依存関係を接続します。
- 依存関係の肥大化を回避しやすくなります。Wireの生成されたコードは必要な依存関係のみをインポートするため、バイナリに未使用のインポートは含まれません。ランタイム依存性インジェクタは、ランタイムまで未使用の依存関係を識別できません。
- Wireの依存関係グラフは静的に認識できるため、ツールと可視化の機会が提供されます。
仕組みは?
Wireには、プロバイダとインジェクタという2つの基本的な概念があります。
プロバイダは、依存関係(関数の引数として記述される)が与えられると値を「提供」する通常のGo関数です。3つのプロバイダを定義するサンプルコードを次に示します。
// NewUserStore is the same function we saw above; it is a provider for UserStore,
// with dependencies on *Config and *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}
// NewDefaultConfig is a provider for *Config, with no dependencies.
func NewDefaultConfig() *Config {...}
// NewDB is a provider for *mysql.DB based on some connection info.
func NewDB(info *ConnectionInfo) (*mysql.DB, error) {...}
一般的に一緒に使用されるプロバイダは、ProviderSets
にグループ化できます。たとえば、*UserStore
を作成する際にデフォルトの*Config
を使用することが多いため、NewUserStore
とNewDefaultConfig
をProviderSet
にグループ化できます。
var UserStoreSet = wire.ProviderSet(NewUserStore, NewDefaultConfig)
インジェクタは、依存関係の順序でプロバイダを呼び出す生成された関数です。必要な入力引数を引数として含むインジェクタのシグネチャを記述し、結果の構築に必要なプロバイダまたはプロバイダセットのリストを使用してwire.Build
への呼び出しを挿入します。
func initUserStore() (*UserStore, error) {
// We're going to get an error, because NewDB requires a *ConnectionInfo
// and we didn't provide one.
wire.Build(UserStoreSet, NewDB)
return nil, nil // These return values are ignored.
}
これで、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 // These return values are ignored.
}
これで、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を最適な言語にするために、皆様と協力できることを楽しみにしています。