ScalaでDIします。Google GuiceというDIコンテナを用いて,Play Framework上で,またより一般的なScalaアプリケーション上でDIを行う方法について説明します。
Dependency Injection
DIの概要は様々な良いエントリがたくさんあるのでさっと流します。
DIとはDependency Injectionの略語で,依存性注入と訳します。依存性というのはどういうことかというと,あるモジュールが別のモジュールを呼び出すとき,「呼び出す側のモジュールが」「呼び出される側のモジュールに」依存します。呼び出される側のモジュールがなければ,呼び出す側が成り立ちません。これが依存性です。
しかしこれだとテストなどがやりにくい。例えば本棚WebアプリケーションのコントローラがMySQLにアクセスするモジュールを呼び出すようなとき,その関係がコード上で固定されてしまっていると,コントローラだけのテストができないわけです。コントローラをインスタンス化すると勝手にMySQLのモジュールがいっしょに作成されてしまい,切り離してテストすることができない。
そこで呼び出すモジュールを外部からいい感じに差し替えられる,すなわち依存性を注入する,すなわちDIする仕組みが模索されました。
ナイーブなDI (コンストラクタ・インジェクション)
このための簡易な方法を考えてみましょう。例えばあるクラスが別のクラスに依存しているとき,これをクラス内部でインスタンス化して使うのではなく,クラスのコンストラクタを経由してインスタンスをもらって使う,という手法があります。これをコンストラクタ・インジェクションConstructor injectionと呼びます。コンストラクタ・インジェクションでは,コンストラクタを通じて実際に使うクラスを注入します。
// 普通の依存関係 class A () { def useFeature() = { new Feature().somefunction() } } // コンストラクタ・インジェクション class A (someFeature: Feature) { def useFeature() = { someFeature.somefunction() } }
さて,ここで一段階trait
を設けて抽象化します。コンストラクタで具体的な実装を受け取っていては結局そのクラスしか受け取れず,クラスを差し替えられないためです。
- 何らかの機能を抽象化した
trait
をインターフェイスとしてまず作ります。 - 実際にその機能を実現するクラスには,
trait
を継承extends
してインターフェイスを実装させます。 - 機能を使いたいクラスは,機能を実現するクラスをコンストラクタで受け取りますが,コンストラクタで受け取る型として,機能を実現するクラスの代わりに,抽象化された
trait
を指定します。 - コンストラクタに具体的なクラスのインスタンスを渡してやります。
こうすることで,実際に使う実装を差し替えることができるようになりました。
しかしこれではコンストラクタでインスタンスをずっと引き回すことになります。最終的にはプログラムのエントリポイントで全てのクラスが具象クラスとして解決されるのですが,エントリポイントから実際に使う箇所までずっとインスタンスを連れていく必要があります。この場合,それを経由する中間のクラスは,渡されるクラスについて何の責務もないのに「もらったインスタンスを作成するクラスのコンストラクタに渡してやる」という余計な仕事をさせられるわけで,かえってコードが密結合になってしまいます。これが素朴なコンストラクタ・インジェクションの欠点です。
DIコンテナ,DI手法
素朴な手法でもDIができることを先程示しましたが,複雑なアプリケーション開発のために,より柔軟にDIする,つまり「上位層はインターフェイスに依存させて下位の層とは切り離すが,いずれ下位の層とどこかで結び付ける」ことが可能な技法やライブラリが開発されています。DIコンテナやDI手法と呼ばれるものです。先述したコンストラクタ・インジェクションも簡易的なDI手法の1つです。DIコンテナ・手法により,コード上の見た目の関係を疎にしつつも,実行時までのどこかのタイミングでインターフェイスと実装とを結び付け,想定した下位の層が呼ばれるようにできます。
DIコンテナやDI手法は,アプリケーションの要求に応じて選択されるべきもので,どのDIコンテナや手法が優れているとか,劣っているということはありません。動的・あるいは静的に,具象クラスを差し替えることができるならば,それらはすべてDIコンテナ・手法です。しかしそれぞれに特徴があり,静的に差し替えることで型チェックの恩恵を受けられるとか,リフレクションが不要だとか,設定が柔軟だとか,さまざまなメリットが強化されています。好きなものを選ぶと良いでしょう。
ちなみにScalaのDI手法としてはCake Patternというものが有名で,これはリフレクションといった「魔法」を使わずにScalaネイティブな機能でDIを実現するもので,非常に持て囃されたり,disられたりしました。賛否両論がたくさんあるということは,それなりに実績のある枯れた技法ということです。
さて最近私はPlay Framework 2.7を使ったWebアプリケーションを開発しています。Playは標準でGoogle Guice(ジュースと発音する)をDIコンテナとして採用しています。Guiceはコンストラクタ・インジェクションベースのDIコンテナです。Guiceは,JavaにおけるDIアノテーションを規定する規格であるJSR330に準拠しており,JSR330のアノテーションで依存性を定義することができます。 今回はGuiceを使ってPlayアプリケーションのインフラストラクチャモジュールを抽象化し,DIするコードを紹介します。
GuiceでDI
最終目的は「あるインフラ層のクラスをDIで動的に注入し,コントローラからは実装を隠す」とします。 プロダクションコードとテストコードとで,インフラ層のクラスを差し替えられれば成功です。
DIのために以下のタスクを実行します。
- リポジトリクラスのインターフェイスとなる
trait
を作成 - インフラ層の具象リポジトリクラスは↑の
trait
を実装する形式にする - コントローラに依存性の記述を行う
- DIの関係性(つまり実際にどのクラスが注入されるのか)を記述した「Guiceモジュール」を作成する
- 実行時にこのGuiceモジュールが呼ばれ,依存性が注入されるようにする
GuiceはPlayに同梱されているのでここでは既にライブラリが使えるという体で説明します。Play以外のフレームワークでGuiceを使いたい場合は,build.sbt
への記述などでGuiceを使える状態にする必要があります。
trait
以下のようなtraitを作成します。
// app/domains/repositories/BookRepository.scala package domains.repositories import domains.models.Book // こういうモデルがあるという想定 trait BookRepository { def findByIds(ids: Seq[BigInt]): Seq[Book] def create(books: Seq[Book]): Unit }
インフラ層の1クラス
先程作成したtrait
に従わせます。
// app/infrastructures/BookRepository.scala package infrastructures import domains.repositories.{BookRepository => BookRepositoryInterface} class BookRepository extends BookRepositoryInterface { def findByIds(ids: Seq[BigInt]): Seq[Book] ={ /* ここでDBアクセスする */ } def create(books: Seq[Book]): Unit = { /*ここでDBアクセスする*/ } }
コントローラ
コントローラは既にコンストラクタに依存性注入のための記述が行われているので,単に引数を追加します。infrastructures
パッケージに全く依存していないことに注目してください。@Inject()
がJSR330アノテーションです。
// app/controllers/FooBarController.scala /* ... */ @Singleton class FooBarController @Inject()(cc: ControllerComponents/* ここから追加 */, bookRepo: BookRepository/*ここまで追加*/) extends AbstractController(cc) { def index() = Action { implicit request: Request[AnyContent] => val books = bookRepo.findByIds(Seq(123456789)) // ここで呼ぶ /* ... */ } }
Guiceモジュール
注入するクラスと注入されるクラスとが完成しました。しかし実行時に具体的にどの実装が使われるのかはまだ定義されていません。Guiceモジュールというものを作成し,どの実装がtraitに注入されるのかを定義します。
ここではinfrastructures.BookRepository
がdomains.repositories.BookRepository
として注入されます。
// app/DI.scala package DI import javax.inject.Inject import com.google.inject.{AbstractModule, Guice, Injector} import domains.repositories.BookRepository // Guiceモジュール class InfrastructureModule extends AbstractModule { override def configure(): Unit = { // 使う実装を接続する // domains.repositories.BookRepositoryは,infrastructures.BookRepositoryの実装が注入される bind(classOf[BookRepository]).to(classOf[infrastructures.BookRepository]) // 他にも注入する組合せがあれば,ここで指定していく // bind(classOf[PiyoRepository]).to(classOf[infrastructures.PiyoRepository]) } }
実行時設定
さて,traitとそれを実装する実際の実装との組合せが定義されましたが,定義しただけでは実行時に使われません。
conf/reference.conf
に,実行時に使われるGuiceモジュールを指定しましょう。reference.conf
はapplication.conf
よりも優先度が低いので,必要があれば上書きすることができます。
// reference.conf // ふつうのDI構成 play.modules.enabled += "DI.InfrastructureModule"
これでplayを実行すると,動的にクラスが注入されて,きちんとinfrastructures.BookRepository
が呼び出されます。
テスト時
依存される側のクラスを直接使うクラスをテストする場合は,普通にコンストラクタに渡すのが最も使いやすいでしょう。
class DummyBookRepo extends domains.repositories.BookRepository { val hash = scala.collection.mutable.Map[Id.Id, Book]() def create(Books: Seq[Book]): Unit = { books.foreach(book => hash += ((book.id, book))) } def findByIds(ids: Seq[Id.Id]): Seq[Book] = ids.flatMap(hash.get) } class DisplayedExhibitsControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { val controller = new DisplayedExhibitsController( stubControllerComponents(), new DummyItemRepo() // ここでモックを作って渡す ) /* ... */ }
直接インスタンス化する関係にない場合は,Guiceのインジェクション機能を使います。Play frameworkではplay.api.inject.guice
パッケージに便利な色々が入っています。
import play.api.{Play, Application} import play.api.inject.guice._ import play.api.inject.bind import org.scalatestplus.play._ import org.scalatestplus.play.guice._ class DummyBookRepo extends domains.repositories.BookRepository { val hash = scala.collection.mutable.Map[Id.Id, Book]() def create(Books: Seq[Book]): Unit = { books.foreach(book => hash += ((book.id, book))) } def findByIds(ids: Seq[Id.Id]): Seq[Book] = ids.flatMap(hash.get) } class BooksControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { // 既存のPlayのインジェクション設定を上書きする implicit override def newAppForTest(testData: org.scalatest.TestData): Application = new GuiceApplicationBuilder() .overrides(bind[domains.repositories.BookRepository].to[DummyBookRepo]) .build() /* ... */ // するとnewする代わりにこのようにインスタンスを生成できる val controller = inject[BooksController] // このcontrollerは既に中で使っている依存性が上書きされている val home = controller.index().apply(FakeRequest(GET, "/")) /* ... */ }
直接Controllerをインスタンス化しても良いのですが,Playはguiceのインジェクション機構をラップしたものを提供しており,これはアプリケーションのテストに必要な「標準的な」依存性(application.conf
からの注入やroutes
の注入など)を自動で注入してくれます。Playのコントローラには様々な依存関係があり,手でインスタンス化するのは骨が折れるからです。
Playの機構によらない,Guiceだけ使った注入
ここまでPlayを前提としたDIについて説明しましたが,Playによらない,Guiceだけを使ったDIをここで説明します。
手順は簡単で,空のGuiceモジュールであるAbstractModule
をもとに,そのconfigurate
メソッドをオーバーライドして,これを注入するinjectorを作成し,injectorのgetInstance
を呼ぶというプロセスでインスタンスが得られます。
実行可能なサンプルを用意しました。sbt test
で実行できます。
// build.sbt /* ... */ libraryDependencies += "com.google.inject" % "guice" % "4.0" /* ... */
// Example.scala package example import javax.inject._ // JSR330 import com.google.inject.AbstractModule // Guiceモジュール class HelloClass @Inject() (val greeter: Greeting) { def sayHello() { println(greeter.greeting) } } trait Greeting { val greeting: String; } class EnglishGreeting extends Greeting { val greeting: String = "Hello!" }
// ExampleSpec.scala package example import org.scalatest._ import com.google.inject._ // DI用のシンタックスシュガー import com.google.inject.AbstractModule // Guiceモジュール class GreetingModule extends AbstractModule { override protected def configure() { bind(classOf[Greeting]).to(classOf[EnglishGreeting]) } } class MockedGreeting extends Greeting { val greeting: String = "Mock!" } class HelloSpec extends FlatSpec with Matchers { "The HelloClass" should "returns normal response" in { // Guiceモジュールを作成する。 // 既存のGuiceモジュールを流用してもよい。 val module = new AbstractModule { def configure(): Unit = { bind(classOf[Greeting]).to(classOf[EnglishGreeting]) } } // 既存のものを使う場合 // val module = new GreetingModule() val injector: Injector = Guice.createInjector(module) val helloInstance: HelloClass = injector.getInstance(classOf[HelloClass]) helloInstance.greeter.greeting shouldBe "Hello!" } "The HelloClass" should "returns mocked response" in { // Guiceモジュールを作成する。 // 既存のGuiceモジュールを流用してもよい。 val module = new AbstractModule { def configure(): Unit = { bind(classOf[Greeting]).to(classOf[MockedGreeting]) } } val injector: Injector = Guice.createInjector(module) val helloInstance: HelloClass = injector.getInstance(classOf[HelloClass]) helloInstance.greeter.greeting shouldBe "Mock!" } }
まとめ
Play上で,あるいはPlayによらない一般的なScalaアプリケーション上で,Guiceを使った依存性注入を行うことができました。より詳しい応用的なDIについては,Guiceのマニュアルを参考にしてください。