The Go Blog

Go CloudのWireによるコンパイル時依存性注入

Robert van Gent
2018年10月9日

概要

Goチームは最近、Go Cloudというオープンソースプロジェクトを発表しました。これは、ポータブルなクラウドAPIとオープンクラウド開発ツールを提供します。この記事では、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) {...}

この手法は小規模では非常にうまく機能しますが、大規模なアプリケーションでは複雑な依存性グラフを持つことがあり、その結果、順序依存ではあるものの、それ以外にはあまり興味を引かない大きな初期化コードブロックが生まれます。特に一部の依存性が複数回使用されるため、このコードをきれいに分割することは難しいことがよくあります。あるサービスの特定の実装を別のものに置き換えることは、新しい依存性のセット(とその依存性…)を追加し、不要になった古いものを削除することで依存性グラフを変更する必要があるため、面倒な場合があります。実際、大規模な依存性グラフを持つアプリケーションの初期化コードに変更を加えることは、退屈で時間がかかります。

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のdigFacebookの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で行われているため、issueを提出して改善点を教えていただくことができます。プロジェクトの更新情報や議論については、Go Cloudメーリングリストにご参加ください。

Go CloudのWireについて時間を割いて学んでいただきありがとうございます。ポータブルなクラウドアプリケーションを開発する開発者にとって、Goを選択言語にするために皆様と一緒に取り組むことを楽しみにしています。

次の記事:App Engineの新しいGo 1.11ランタイムを発表
前の記事:2018年Go企業アンケートにご参加ください
ブログインデックス