FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

PlayFramework 2.6.X のDIについて

これはScala Advent Calendar 2017の11日目の記事です。

こんにちはセプテーニオリジナルの池田です。

弊社では社内勉強会が定期的に開かれており
先月 @kawachiさんより「DIを正しく知って便利に使おう」という発表がありました。

私自身社内での勉強会を受ける前までは、javax.injectGuiceのDIが使われる背景やメリットをあまり理解していませんでしたが、発表を聞いて、DIの歴史的な背景やPlayでのベストプラクティスな使い方など勉強になりました。

今回は、勉強会の復習にPlayFrameworkのDI周りについて書きます。

目次

1.そもそもPlayFrameworkでDIが使われる背景

昔のPlayでは現在のapplicationをグローバル変数的に保持しており(play.api.Play.current)、至る所で参照しているためテストしづらい状態でした。

そこでPlayではグローバルな状態に依存することを取り除くことにしました。

グローバルな状態を削除することで次のような利点があります。

  • アプリケーションのテストが簡単
  • 単一のJVMの中に複数のPlayインスタンスや、軽量のPlayアプリケーションを埋め込むことなどができる
  • ApplicationLifecycleがより簡単に理解しやすく、推定しやすくなる。

この第一歩として、PlayではDIを使うことにしました。
参考:Dependency Injection

2.DI(Dependency Injection)とは

Dependency injectionとは、デザインパターンの一つです。こちらの記事にわかりやすく紹介されていますが、 英語のwikiDependencyとは「オブジェクト」という意味で定義されており

簡単に言うと、あるオブジェクト(=サービス)を別のオブジェクト(=クライアント)に渡すパターンがDIパターンです。

3.PlayFrameworkで提供されているDIのアプローチ

公式ドキュメントによると、PlayFrameworkでは以下のアプローチを提供しています。

  • Guiceをそのまま使う方法
  • JSR 330アノテーションを使う方法
  • プレーンコンストラクタやファクトリメソッドを使う方法
  • Cakeパターンを使う方法

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の背景や使い方が勉強できました。