これはScala Advent Calendar 2017の11日目の記事です。
こんにちはセプテーニオリジナルの池田です。
弊社では社内勉強会が定期的に開かれており
先月 @kawachiさんより「DIを正しく知って便利に使おう」という発表がありました。
私自身社内での勉強会を受ける前までは、javax.inject
やGuice
のDIが使われる背景やメリットをあまり理解していませんでしたが、発表を聞いて、DIの歴史的な背景やPlayでのベストプラクティスな使い方など勉強になりました。
今回は、勉強会の復習にPlayFrameworkのDI周りについて書きます。
目次
- 1.そもそもPlayFrameworkでDIが使われる背景
- 2.DI(Dependency Injection)とは
- 3.PlayFrameworkで提供されているDIのアプローチ
- 4.Playにおけるベストプラクティス
- 5.PlayFrameworkでDI
- 6.終わりに
1.そもそもPlayFrameworkでDIが使われる背景
昔のPlayでは現在のapplicationをグローバル変数的に保持しており(play.api.Play.current
)、至る所で参照しているためテストしづらい状態でした。
そこでPlayではグローバルな状態に依存することを取り除くことにしました。
グローバルな状態を削除することで次のような利点があります。
- アプリケーションのテストが簡単
- 単一のJVMの中に複数のPlayインスタンスや、軽量のPlayアプリケーションを埋め込むことなどができる
- ApplicationLifecycleがより簡単に理解しやすく、推定しやすくなる。
この第一歩として、PlayではDIを使うことにしました。
参考:Dependency Injection
2.DI(Dependency Injection)とは
Dependency injectionとは、デザインパターンの一つです。こちらの記事にわかりやすく紹介されていますが、 英語のwikiでDependencyとは「オブジェクト」という意味で定義されており
簡単に言うと、あるオブジェクト(=サービス)を別のオブジェクト(=クライアント)に渡すパターンがDIパターンです。
3.PlayFrameworkで提供されているDIのアプローチ
公式ドキュメントによると、PlayFrameworkでは以下のアプローチを提供しています。
4.Playにおけるベストプラクティス
勉強会では
- Playを使いたい
- Playに依存しないアプリにも使いたい
- ボイラープレートを減らしたい
上記を考慮すると以下のDIの使い方がバランスが良いのではないかという話でした。
・機能実装はコンストラクタにJSR 330アノテーションをつける ・Guiceモジュールを書く
DIのアノテーションはGuiceにもありますが、Guiceを使うとGuiceのみしか使えなくなります。 JSR 330のアノテーションを使うことで、JSR 330の規格を継承しているGuiceも使えるので、 JSR 330のアノテーションを使うのがベストプラクティスだそうです。*1
また他にもDIを行う上でのベストプラクティスを共有してもらいました。
・モジュール(bindingの提供)は細かく保つ ・単体アプリでは、Guice.createInjector()で注射器を作る
5.PlayFrameworkでDI
ということで、実際に上記のまとめに沿ったコードを書いて行きたいと思います。
package services import javax.inject.Inject import com.google.inject.{AbstractModule, Guice, Injector} trait Marathon { def value: String } class TokyoMarathon() extends Marathon { override val value: String = "Tokyo" } //Guiceモジュール class TokyoMarathonModule extends AbstractModule { override def configure(): Unit = { //Marathonが必要な時はTokyoMarathonを使う bind(classOf[Marathon]).to(classOf[TokyoMarathon]) } } class HonoluluMarathon() extends Marathon { override val value: String = "Hawai" } class HonoluluMarathonModule extends AbstractModule { override def configure(): Unit = { bind(classOf[Marathon]).to(classOf[HonoluluMarathon]) } } trait MarathonRace { def venue(): Unit } // MarathonRaceImplは、Marathonに依存することをJSR330のアノテーションで表現 // @Inject 注入可能なこと示す class MarathonRaceImpl @Inject()(marathon: Marathon) extends MarathonRace { override def venue(): Unit = println(marathon.value) } class MarathonRaceModule extends AbstractModule { override def configure(): Unit = { //MarathonRaceが必要な時はMarathonRaceImplを使う bind(classOf[MarathonRace]).to(classOf[MarathonRaceImpl]) } } object Main { def main(args: Array[String]): Unit = { //単体アプリなので、Guice.createInjector()で注射器を作る val injector: Injector = Guice.createInjector(new TokyoMarathonModule, new MarathonRaceModule) val marathonRace: MarathonRace = injector.getInstance(classOf[MarathonRace]) marathonRace.venue() //依存性注射器は複数作れ、モジュールの組み合わせも自在 val injector2: Injector = Guice.createInjector(new HonoluluMarathonModule, new MarathonRaceModule) val marathonRace2: MarathonRace = injector2.getInstance(classOf[MarathonRace]) marathonRace2.venue() } }
実行結果
Tokyo Hawai
application.confをDIしたいとき
またPlayを使っていてapplication.confから値を読み込んでbindingする場合は以下のようにできます。
application.conf
api.google.applicationName = "Google Sheets API Scala Sample"
class GoogleSheetApiModule(environment: Environment, configuration: Configuration) extends AbstractModule { @Override def configure(): Unit = { bind(classOf[String]) .annotatedWith(Names.named("applicationName")) .toInstance(configuration.get[String] "api.google.sheets.applicationName" ) }
//@Singleton 一度しかインスタンス化されないことを指定 @Singleton class GoogleSheetApiClient @Inject()( //@Named 注入するオブジェクトを文字列の名前で識別する @Named("applicationName") applicationName: String, ) { ・・・ new Sheets.Builder(httpTransport, jsonFactory, credential).setApplicationName(applicationName).build ・・・ }
6.終わりに
私自身社内での勉強会を受ける前までは、DIの背景やメリットがあまりよくわかっていませんでしたが 勉強会やブログを書きながらDIの背景や使い方が勉強できました。
- 参考
- Play 2.4 と Dependency Injection
- Scalaにおける最適なDependency Injectionの方法を考察する 〜なぜドワンゴアカウントシステムの生産性は高いのか〜
- Minimal Cake Pattern のお作法
- 実戦での Scala: Cake パターンを用いた Dependency Injection (DI)
- 猿でも分かる! Dependency Injection: 依存性の注入
- DI・DIコンテナ、ちゃんと理解出来てる・・?
- やはりあなた方のDependency Injectionはまちがっている。
- ScalaのDIにMacWireを使う
- Google Guice 使い方メモ