これは ピックアップ Advent Calendar 2018 24日目の記事です
サーバーサイド寄りエンジニアの @sakushin です。Qiita初投稿です。
業務では主として Go を利用して Web API やバッチサービスの開発を行っています。
複雑度の高いシステムでは、アプリケーションのレイヤ化・コンポーネント化を促進するためDIのプラクティスを取り入れています。
最近ひとつのプロジェクトで試験的に導入したDI補助ツール Wire がよく手に馴染むと感じているため、今回はこれをテーマに話をしたいと思います。
go-cloud付属のGolang用DIツール "Wire" が素晴らしい。実装とインターフェースのワイヤリングに集中できるAPIで、生成されるコードも簡潔。依存関係の変更(=ファクトリメソッドのシグネチャ変更)にも強い。 https://t.co/lrehTcAbVo
— コハク酸エステル (@sakushin) July 30, 2018
導入や詳細な利用方法は公式チュートリアルや他の方の記事のまとまりが良いため割愛し、ここではごく簡単な例示でその効用を所感を交えつつ解説します。
TL;DR
- 愚直なDIロジックは読み手にとってフレンドリーだが、書き手には無駄な負担がある
- Wire の利用で書き手の負担を大幅に軽減できる
- Wire の記述ルールや生成コードはシンプルであり読み手にとっても必要十分
想定課題
以下のようなコンポーネント(interface/struct)と依存関係が存在すると仮定します。
ひとつのコンポーネントは複数のコンポーネントに依存する可能性があり、また複数のコンポーネントに依存される可能性があります。
ここでは Server
を初期化してその機能を呼び出すことを目的とします。
尚、すべてのコンポーネントについて、自身が依存するコンポーネントを引数とするファクトリメソッドが component
パッケージ以下に存在する(=コンストラクタインジェクションが可能な状態である)と仮定します。
愚直にやる
まず、愚直に書くとコードは例えば以下のような形になるでしょう。
package main
import (
. "github.com/sakushin/wire-example/component"
)
func main() {
// 各種コンポーネントの手動インジェクション
// 依存ツリーの葉の部分から依存関係を意識しつつ完全なワイヤリングをする必要がある
currentTimeProvider := NewCurrentTimeProvider()
repository1 := NewRepository1(currentTimeProvider)
repository2 := NewRepository2(currentTimeProvider)
application1 := NewApplication1(currentTimeProvider, repository1)
application2 := NewApplication2(currentTimeProvider, repository2)
handler1 := NewHandler1(application1)
handler2 := NewHandler2(application2)
handler3 := NewHandler3(application1, application2)
server := NewServer(handler1, handler2, handler3)
// インジェクションが完了したコンポーネントにディスパッチ
server.Start()
}
愚直に書くのは良いことです。
読み手にはコンポーネントの依存関係とその初期化方法がシンプルに伝わります。
しかし、書き手の立場ではどうでしょう?
コンポーネントの初期化は他のコンポーネントに依存しないに依存しないなものから順次、新たに解決可能なものを探りつつ行わなければなりません。
関数分割とメモ化によって初期化順序を考えないようにする方法も考えられますが、その分冗長性と複雑度が増してしまいます。
この程度の規模であればメンテは容易ですが、コンポーネント数の増加に従い機能追加やリファクタリングに要するコストも無意味に増加していく事でしょう。
Wire を利用する
依存関係グラフの特性上、前提条件として示した すべてのコンポーネントについて、自身が依存するコンポーネントを引数とするファクトリメソッドが存在する という状況であれば、コンポーネントのファクトリメソッドを列挙すれば 任意のコンポーネントの生成ロジックは機械的に定める事ができる はずです。
そういった要求を実現するためのフレームワークとしてDIコンテナが遍く存在し、そしてそれを Go で実現するためのツールのひとつとして今回紹介する Wire があります。
Wire は特別なビルドタグをつけた Go のコードをデータソースとして、コンパイルタイムでインジェクタのコードを生成します。
インジェクタの生成ルールを記述したコードは以下の通りです。
// +build wireinject
package main
import (
"github.com/google/wire"
. "github.com/sakushin/wire-example/component"
)
// 自動的にインジェクションを行いたいコンポーネントの型が戻り値のメソッドを定義する
// Wireにヒントを与えるための手続き
func InitializeServer() Server {
// インジェクション対象と依存コンポーネントのファクトリメソッドの参照を列挙する
// 並び順は任意
wire.Build(
NewServer,
NewHandler1,
NewHandler2,
NewHandler3,
NewApplication1,
NewApplication2,
NewRepository1,
NewRepository2,
NewCurrentTimeProvider,
)
// コンパイルエラーを避けるため形式的にnilを返却する
return nil
}
注視するべきところをコメントで補足しています。
自身が利用するファクトリメソッドを列挙すれば任意のコンポーネントの生成ロジックは機械的に定める事ができるはず という論理の通り、wire.Build
メソッドの引数として各コンポーネントのファクトリメソッドの参照を羅列しています。(関数の参照なので実引数は不要)
また、愚直に書いていたときに気をつけていた初期化順についても、ここでは関知していません。
一見して非常にシンプルな形ではありますが、これを元に自動生成されるコードはどのような形になるでしょうか?
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
"github.com/sakushin/wire-example/component"
)
// Injectors from wire.go:
func InitializeServer() component.Server {
currentTimeProvider := component.NewCurrentTimeProvider()
repository1 := component.NewRepository1(currentTimeProvider)
application1 := component.NewApplication1(currentTimeProvider, repository1)
handler1 := component.NewHandler1(application1)
repository2 := component.NewRepository2(currentTimeProvider)
application2 := component.NewApplication2(currentTimeProvider, repository2)
handler2 := component.NewHandler2(application2)
handler3 := component.NewHandler3(application1, application2)
server := component.NewServer(handler1, handler2, handler3)
return server
}
生成結果もまた素直な形となりました。
愚直に手動で記述していた例と比べても、差異がほとんどありません。
通常こういったフレームワークの魔術的な仕組みはしばしば読み手を困らせます。
しかし Wire は事前にコード生成を行うアプローチであり、ロジックが自明である事に加え、生成されるコードも十分リーダブルであるため、 チームで導入する場合も合意形成しやすいように思えます。
個人的にはこの単純さによってもたらされる保守性の高さやチームでの導入難易度の低さが Wire の最も特筆するべき点だと思っています。
最後に自身が用意するディスパッチャのコードは上記を受けて以下のような形になります。
package main
func main() {
// 自動生成されたインジェクタからインジェクションを実行
server := InitializeServer()
// インジェクションが完了したコンポーネントにディスパッチ
server.Start()
}
自動生成されたインジェクション関数を呼び出すだけの、非常に簡潔な形になりました。
依存コンポーネントを追加する
例えば多くのコンポーネントが新たに依存するようなコンポーネント Hoge
をあとから追加する場合、各コンポーネントのファクトリメソッドの定義変更以外に行うべきことは、wireのコンポーネント列挙ブロックに Hoge
のファクトリメソッドを1行加え、コード生成を再実行するだけです。
コード生成時に必要なコンポーネントの列挙が不足していたとしても Wire はその旨をわかりやすくアラートしてくれるため、大きな改修やリファクタリングの実施時にもリスクやコストとなりにくいです。
終わりに
今回は主に Wire の導入コストの低さとシンプルさに注目していましたが、得られる結果がシンプル故にロックイン度合いが低くなるというのも注目するべき点だと思います。
Go 他の多くの言語と比べては抽象化に関する表現力は高いとは言えませんが、Wire や stringer など必要に応じてコード自体を生成する事によって抽象度を高めるといったアプローチがしばしば見られます。
生成されたコードは冗長になりますが、常に再生成が可能なことからその冗長さについて悲観する必要はなく、また当然 Go の表現力を越えないため多くの人間にとって理解しやすいといったメリットがあります。
筆者は Go に触れ始めてまだ1年足らずではありますが「Goに入ってはGoに従え」の精神を持ち続ける限り、特にチームでの開発において問題の本質に向かいやすく、手に馴染む道具であると感じています。