こんにちは。エンジニアのNishikawaです。
今日はGoogle Guiceについて色々調べて実際に使ってみたので、そのことについて書きます。
Google Guiceとは
Google GuiceはGoogleが開発しているDI用のフレームワークです。結構昔から存在しているフレームワークで初出は2007年とWikipediaにはあります。
最新は2018.11.14 時点では4.2.2です。
Wiki
https://ja.wikipedia.org/wiki/Google_Guice
GitHub
https://github.com/google/guice
Google Guiceは、後ほど紹介しますが結構簡単にDIができるようになっています。そのためScala界隈だとPlay Frameworkに使われています。
Play Frameworkを初めて使った人のなかにはDIの仕組みに戸惑った人も中にはいるかと思いますが、この記事を少しでも参考にしていただければ幸いです。
どうやって使うのか
詳しい使い方は公式のユーザガイドを見ていただければと思いますが、とりあえず使うには以下のポイント(DIの基本ですが)を抑えれば大丈夫です。
- 注入する依存性の作成
- 依存性の注入先の作成
- 依存性と注入先のバインド
以上をScalaコードに落とすと以下になります。
import com.google.inject.{AbstractModule, Guice} import javax.inject.{Inject, Singleton} class Dependency { def print: Unit = println("Hello!") } @Singleton class Receiver @Inject()(dependence: Dependency) { def execute: Unit = dependence.print } object Executor { def main(args: Array[String]): Unit = { val injector = Guice.createInjector(new AbstractModule{}) val instance = injector.getInstance(classOf[Receiver]) instance.execute } }
DIの旨味のないコードになっておりますが、使うとしたらこれが最小の構成なのではないかなと思います。
Guiceは「Guice.createInjector」メソッドでインジェクタを作成する際にAbstractModuleを継承したクラスのインスタンスを必要としており、ここに依存関係の設定を記載することで、インスタンス生成時にそれらのクラスをバインディングした状態でインスタンスを生成してくれます。
この際、上記のように特に設定を記載しない場合は勝手に存在するクラスをバインドしてくれます。
実行すると以下のようになります。
Hello!
上記のコードをもう少し汎用的にしてみます。先ほど自動でバインディングされていた「Dependency」クラスをトレイトに変更し、それを継承した「DependencyImpl1」と「DependencyImpl2」クラスを作成します。トレイトには実装は何もせず、Impleに対してメソッドを実装します。
mainメソッドにはそれぞれの依存関係の設定を記載したインジェクタを作成し、そこからインスタンスを呼ぶようにします。それが以下のコードです。
import com.google.inject.{AbstractModule, Guice} import javax.inject.{Inject, Singleton} trait Dependency { def print: Unit } class DependencyImpl1 extends Dependency { def print: Unit = println("Dependency1 - Hello!") } class DependencyImpl2 extends Dependency { def print: Unit = println("Dependency2 - Hello!") } @Singleton class Receiver @Inject()(dependence: Dependency) { def execute: Unit = dependence.print } object Executor { def main(args: Array[String]): Unit = { val injector1 = Guice.createInjector(new AbstractModule{ override def configure(): Unit = { bind(classOf[Dependency]).to(classOf[DependencyImpl1]) } }) val instance1= injector1.getInstance(classOf[Receiver]) instance1.execute val injector2 = Guice.createInjector(new AbstractModule{ override def configure(): Unit = { bind(classOf[Dependency]).to(classOf[DependencyImpl2]) } }) val instance2 = injector2.getInstance(classOf[Receiver]) instance2.execute } }
ここで注目して欲しいのが、インジェクタ呼び出しの際に記載している「AbstractModule」です。ここで「configure」メソッドをオーバライドしており、この中に依存関係の設定を記載しております。
上記を実行すると以下のようになります。
Dependency1 - Hello! Dependency2 - Hello!
現場でどうやって使ったか
DIは現場ではよくテスタビリティやメンテナンス性を上げるために利用されます。
今回は、アプリケーション開発を行う上でBigQueryに接続するために作成したDAOの部分でDIを利用しました。
簡単な構成
考え方としては、DAOは発行するクエリだけを保持して下位のレイヤーに私検索を行うようにしようと考えました。 その過程でSQLの実行環境が時と場合によって変わることを想定し、ExecutionEnvironment トレイトを作成しました。 今回は実際の環境ではBigQueryを、テストではH2を使用したかったため、このExecutionEnvironmentトレイトを継承した「GoogleApiServicesBigQueryExecutionEnvironment」クラスと「ScalikeJDBCExecutionEnvironment」を作成しました。
実装したBigQueryに対するクエリ発行のコードは以下のようになります。(コード自体は色々と長いのでDIに関係しそうなところだけ記載しております)
trait ExecutionEnvironment { def find(query: QueryTrait): List[Map[String, Any]] def count(query: QueryTrait): Long } trait EnvironmentConfig { ... } trait QueryTrait { ... } class Query extends QueryTrait { ... } class GoogleApiServicesBigQueryExecutionEnvironment @Inject()(envConf: EnvironmentConfig) extends ExecutionEnvironment { override def find(query: QueryTrait): List[Map[String, ANy]] = { // BigQuery用のクエリ発行処理 ... } override def count(query: QueryTrait): Long = { // BigQuery用のクエリ発行処理 ... } } class GoogleApiServicesBigQueryEnvironmentConfig extends EnvironmentConfig { // 接続設定用の処理 ... } case class TestModel(id: Long, name: String) @Singleton class TestDAO @Inject()(environment: ExecutionEnvironment) { def findById(id: Long): List[TestModel] = { // クエリオブジェクトの作成 val query: Query = ... val result = environment.find(query) ... // 後続のデータ整形処理 } }
これに対してテストを行うときはBigQueryではなくh2上で行うのでScalikeJDBCで行いますので以下のようになりました。(こちらもさっきと同じでDIに関するところのみ)
import org.specs2.mutable.Specification class ScalikeJDBCExecutionEnvironment @Inject()(envConf: EnvironmentConfig) extends ExecutionEnvironment { override def find(query: QueryTrait): List[Map[String, ANy]] = { // ScalikeJDBC用のクエリ発行処理 ... } override def count(query: QueryTrait): Long = { // ScalikeJDBC用のクエリ発行処理 ... } } class ScalikeJDBCEnvironmentConfig extends EnvironmentConfig { // 接続設定用の処理 ... } class TestDAOSpec extends Specification { "TestDAO" >> { "IDでレコードを検索する" >> { val injector = Guice.createInjector(new AbstractModule{ override def configure(): Unit = { bind(classOf[ExecutionEnvironment]).to(classOf[ScalikeJDBCExecutionEnvironment]) bind(classOf[EnvironmentConfig]).to(classOf[ScalikeJDBCEnvironmentConfig]) bind(classOf[TestDAO]).asEagerSingleton() } }) val instance= injector.getInstance(classOf[TestDAO]) val result = instance.findById(1) // assertion ... } } }
上記の仕組みにより、本番ではBigQueryを、テストではScalikeJDBCを使用することができるようになったため単体試験がやりやすくなりました。
使ってみての感想
今回Google Guiceを使って見た感想としては、とても簡単という印象でした。設定についてはコードで書いてあげればいいので従来のXMLをたくさん書くみたいなことをしない分、実装もテストもしやすいというメリットがあると個人的には思います。
また、GuiceはPlay frameworkでも使用されているので利用方法を習得することは無駄じゃないと思っております。
学習コストも比較的低かったので、実装のパートナーに説明し展開することも簡単にできました。
以上がメリットです。
デメリットについては、今回プロジェクトで使ってみてとくにありませんでした。
もう少し使い倒せば粗も見つかるのでしょうか、普通に使う分には問題なかったので皆様も新規開発等で短期間で開発を行わないといけなく、クラス設計をする上で悩んだ場合は導入の検討をしてみるのはいいかもしれません。
以上