2019.06.21
GoのおすすめDIツール
はじめに
これまでラクスルではRubyを開発言語として採用することが多かったのですが、最近はコマンドラインツールやバッチ処理などでGoによる開発も増えています。
私が最近取り組んでいる印刷発注基盤の刷新プロジェクトでもGoを使ってWEBアプリケーションを開発しており、社内では先例がないこともあって色々と苦労しながらも楽しく開発を進めています。
GoによるWEBアプリケーション開発では、RubyにおけるRailsのようなデファクトスタンダードは存在しないため考慮すべき点がたくさんあります。
例えば、「パッケージ構成をどうするか」、「WEBアプリケーションフレームワークを使うべきか」などですが今回はちょっと軽めのテーマとしてDIツールについて紹介します。
DIとは
DIとはDependency Injectionの略で「依存性の注入」と訳されます。
Goではインタフェース型の値をコンスタラクタの引数などで渡すことでDIを実現することが多いと思います。
具体的なコードを見てみましょう。
type DogUsecase struct { repo DogRepository } func NewDogUsecase(dogRepo DogRepository) DogUsecase { return DogUsecase{ repo: dogRepo, } } func (u DogUsecase) All() []Dog { return u.repo.All() } type DogRepository interface { All() []Dog } type dogRepositoryImpl struct{} func NewDogRepository() DogRepository { return dogRepositoryImpl{} } func (r dogRepositoryImpl) All() []Dog { // DBからDogを取得 } func main() { // インターフェース型の値を生成 repo := NewDogRepository() // コンストラクタの引数として値を渡す usecase := NewDogUsecase(repo) usecase.All() }
上記ではDogUsecase
のAll()メソッド内でDogRepository
を使っています。
そして、コンストラクタであるNewDogUsecase
の引数としてDogRepository
を渡しています。
ポイントは下記の2点です。
DogUsecase
の内部でDogRepository
を生成しているのではなく外から渡しているDogRepository
をインターフェースにしている
DogUsecase
はDogRepository
というインターフェースにのみ依存しており、その具象型(dogRepositoryImpl
)には依存していません。
そのため、DogUsecase
の単体テストを書く時にDogRepository
の具象型をモックに変更することが可能です。
また、DogRepository
の具象型の実装に変更(データの取得元をDBではなく外部APIに変えるなど)があった場合でも、インターフェースが変わらない限りはDogUsecase
のコードを修正する必要がありません。
このように、DIを使うことによってDBや外部APIといった実装の詳細を利用者側から隠蔽し、モジュール間を疎結合にすることで保守性やテスタビリティを高めることができます。
DIツール(dig)の紹介
上述したコードではmain関数の中でDIを行なっていました。
今回のようなシンプルなコードの場合はこれでも問題ないのですが、アプリケーションが大きくなっていくとDIのためのコードが肥大化しメンテナンスしづらくなります。
そこで、これらのコードを自動化するためのDIツールが役に立ちます。
GoのDIツールはいくつかありますが、現在私たちのプロジェクトではuber-go/digを使っています。
digの特徴は何といってもシンプルで学習コストが低い点です。また、少し複雑なDIもやろうと思えばできる柔軟性も備えています。
先ほどのコードの中でDIしていた箇所をdigを使って書き直すとこのようになります。
func main() { container := dig.New() container.Provide(NewDogRepository) container.Provide(NewDogUsecase) container.Invoke(func(usecase DogUsecase) { usecase.All() }) }
dig.New()でDIコンテナを作成し、Provideでコンテナに型を登録します。
Provide
の引数は、「返り値として登録したい型を返す関数」です。
例えば、NewDogRepository
は返り値としてDogRepository
型を返す関数なので、Provide
の引数として渡すことによってDogRepository
型をコンテナに登録することができます。
登録した型の値を取り出したい場合には、Invokeを使用します。Invoke
の引数は、「利用したい型を引数にした関数」です。
ここでは、DogUsecase
型を使ってAll()
メソッドを実行したいので、DogUsecase
型を引数とする関数がInvoke
の引数になっています。
注目していただきたいのは、container.Provide(NewDogUsecase)という行です。
NewDogUsecase
は引数としてDogRepository
を必要としますが、この行ではDogRepository
を渡していません。
一つ前の行のcontainer.Provide(NewDogRepository)によってDogRepository
型をコンテナに登録しているため、Invoke
の実行時には自動でNewDogRepository
の引数であるDogRepository
型の値をコンテナから取得することができます。
一見するとDIツールを使わなかったコードの方が短くてシンプルに思えるかもしれませんが、アプリケーションが成長してモジュールが増え依存関係も複雑になっていくと、手動でDIをする必要がなくコンストラクタの引数によって自動的にDIできることのメリットは大きくなります。
digの応用的な使い方
digを使った場合、基本的には型をベースにしてDIが実行されますが、時には同じ型で別の値を使いたいというケースもあると思います。
例として、MasterとSlaveのDBを同じRepository内で使いたいというケースを考えてみます。
func NewMasterConnection() (*sql.DB, error) func NewSlaveConnection() (*sql.DB, error) type DBConnection struct { dig.In MasterConn *sql.DB `name:"master"` SlaveConn *sql.DB `name:"slave"` } func NewDogRepository(c DBConnection) DogRepository { return dogRepositoryImpl{ master: c.MasterConn, slave: c.SlaveConn, } } func main() { container := dig.New() container.Provide(NewMasterConnection, dig.Name("master")) container.Provide(NewSlaveConnection, dig.Name("slave")) container.Provide(NewDogRepository) }
上記では、NewMasterConnection
とNewSlaveConnection
をProvide
の引数として渡す際にdig.Nameを使って名前付けしています。
そして、NewDogRepository
ではDBConnection
という構造体のフィールドの中で`name:”master”`や`name:”slave”`といったタグを付ける事で同じ型(*sql.DB
)の中でdig.Name
に対応する値を取得することができます。
このように、digは少し複雑なDIをする場合も柔軟に対応できます。
今回紹介した機能の他にもオプショナルな型であったり、同じ型の異なる値をグループ化したりといったこともできますので、詳しくは公式ドキュメントをご参照ください。
まとめ
- ラクスルでもGoで開発している!
- DIはモジュール間の結合度を下げて保守性やテスタビリティを高める
- 手動DIはアプリケーションが大きくなるとつらいのでDIツールを使うのがオススメ
- uber-go/digというDIツールが便利
GoのおすすめDIツール
Featured posts
in #Technology