1. 序章

最新のフレームワークのほとんどは、依存性注入ライブラリを使用して、オブジェクトを作成、再利用、およびモックする簡単な方法を提供します。 PlayフレームワークにはデフォルトでGoogleGuiceが付属していますが、使用できるDIフレームワークはこれだけではありません。 この記事では、 GoogleGuiceおよびMacWireコンパイル時の依存性注入の最も一般的なDIの使用法のいくつかを示します。

2. 設定

すでに述べたように、PlayにはGuiceが付属しています。 MacWireを使用する場合は、 build sbtファイルに2つの依存関係を追加する必要があります。

libraryDependencies += "com.softwaremill.macwire" %% "macros" % "2.4.0" % Provided
libraryDependencies += "com.softwaremill.macwire" %% "util" % "2.4.0"

3. コンポーネントの定義:単一のオブジェクトと特性

次のセクションでは、シングルトンを定義する方法の例をいくつか示します。

3.1. Guiceを使用する

アノテーションjavax.inject.Singletonを使用してシングルトンを定義する方法を見てみましょう。

@Singleton
class UserService {
  // ...
}

または、シングルトンをモジュールで定義することもできます。

bind(classOf[UserService])
  .in(classOf[Singleton])

3.2. MacWireの使用

一方、MacWireでコンポーネントを配線するには、コンポーネント特性内でwireマクロを使用する必要があります。

lazy val userService = wire[UserService]

同様に、インスタンスを明示的に提供するシングルトンを定義できます。

lazy val userService = new UserService(/*...*/)

4. コンポーネントの定義:コレクション

単一のオブジェクトを定義する方法がわかったので、次に、同じタイプのオブジェクトコレクションをワイヤリングする方法を紹介します。

この例では、 OrderPipelineProcessor traitインスタンスのコレクションをワイヤリングします。

case class Order(id: Long, userId: Long, date: Long)

trait OrderPipelineProcessor {
  def process(order: Order): Unit
}

4.1. Guiceを使用する

ProviderインターフェースまたはProvidesアノテーションを使用して、GoogleGuiceでコレクションをワイヤリングできます。

@Provides
  def orderPipelineProcessors(): Seq[OrderPipelineProcessor] =
    Seq(
      (order: Order) => println("Processor 1 processed"),
      (order: Order) => println("Processor 2 processed"),
      (order: Order) => println("Processor 3 processed"),
      (order: Order) => println("Processor 4 processed")
    )

@Singleton
class OrderService @Inject() (orderPipeline: Seq[OrderPipelineProcessor]) {
  def process(order: Order): Unit = orderPipeline.foreach(_.process(order))
}

4.2. MacWireの使用

SetをMacWireのwireSetマクロで配線できます。

lazy val p1: OrderPipelineProcessor = (order: Order) =>
  println("Processor 1 processed")
lazy val p2: OrderPipelineProcessor = (order: Order) =>
  println("Processor 2 processed")
lazy val p3: OrderPipelineProcessor = (order: Order) =>
  println("Processor 3 processed")
lazy val p4: OrderPipelineProcessor = (order: Order) =>
  println("Processor 4 processed")

lazy val orderPipelineProcessors = wireSet[OrderPipelineProcessor]

lazy val orderService = wire[OrderService]

5. 複数のモジュールを構成する

アプリケーションが時間の経過とともに大きくなる傾向がある場合、モジュールを分割することは常に良い考えです。

5.1. Guiceを使用する

例として、OrderModuleUserModuleの2つのモジュールを作成しましょう。

class OrderModule(environment: Environment, configuration: Configuration) 
  extends AbstractModule {

  override def configure(): Unit = {
    // ...
  }
}

class UserModule(environment: Environment, configuration: Configuration) 
  extends AbstractModule {
  // ...
}

最後に、application.confの有効なモジュールリストにモジュールを追加する必要があります。

play {
  modules.enabled += "modules.UserModule"
  modules.enabled += "modules.OrderModule"
}

5.2. MacWireの使用

対照的に、MacWireを使用してモジュールを構成することは、本質的に特性の積み重ねです。

trait UserComponents { // ... }

trait OrderComponents { // ... }

すべてのコンポーネント特性は、ApplicationLoaderにまとめられています。

class AppComponents(context: Context) extends BuiltInComponentsFromContext(context)
  with BuiltInComponents
  with HttpFiltersComponents
  with UserComponents
  with OrderComponents {
  
  // ...
}

6. 同じタイプの複数のオブジェクトの配線

アプリケーションが同じタイプの複数のオブジェクトを配線することは珍しいことではありません。

6.1. Guiceを使用する

名前付きコンポーネントを使用すると、GoogleGuiceで同じタイプの複数のオブジェクトをバインドできます。

trait OrderValidationService {
  def validate(order: Order): Boolean
}

class BusinessOrderValidationService extends OrderValidationService {
  override def validate(order: Order): Boolean = {
    println("Business order validation")
    true
  }
}

class EnterpriseOrderValidationService extends OrderValidationService {
  override def validate(order: Order): Boolean = {
    println("Enterprise order validation")
    true
  }
}

コンポーネントは、モジュールファイルでのインスタンス定義中に名前が付けられます。

bind(classOf[OrderValidationService])
  .annotatedWith(Names.named("Business"))
  .toInstance(new BusinessOrderValidationService)

bind(classOf[OrderValidationService])
  .annotatedWith(Names.named("Enterprise"))
  .toInstance(new EnterpriseOrderValidationService)

最後に、アプリケーションインジェクターは、@Namedアノテーションを介してインスタンスに名前を付けて検索します。

class OrderService @Inject()(
  @Named("Business") businessOrderValidationService: OrderValidationService,
  @Named("Enterprise") enterpriseOrderValidationService: OrderValidationService,
  orderPipeline: Seq[OrderPipelineProcessor]) {
  
  // ...
}

6.2. MacWireの使用

MacWireの修飾子またはタグは、インスタンスを区別するためのフレームワークの方法です。

trait Business
trait Enterprise

lazy val businessOrderValidationService: BusinessOrderValidationService @@ Business =
  (new BusinessOrderValidationService).taggedWith[Business]
lazy val enterpriseOrderValidationService: EnterpriseOrderValidationService @@ Enterprise =
  (new EnterpriseOrderValidationService).taggedWith[Enterprise]

タグ付きの依存関係は、同じタグを使用するコンストラクターで使用できます。

class OrderService(
  businessOrderValidationService: OrderValidationService @@ Business,
  enterpriseOrderValidationService: OrderValidationService @@ Enterprise,
  orderPipeline: Set[OrderPipelineProcessor]
) { // ... }

7. モジュールを使用したテスト

テストでは、モジュールを部分的または完全にモックすることが理にかなっている場合があります。 たとえば、アプリケーションを完全にテストする必要があるかもしれませんが、データベースやWebサービスなどの外部システムを呼び出したくありません。

まず、リモートAPIに依存するサービスを定義しましょう。

trait RemoteApi {
  def remoteCall(): String
}

class ServiceWithRemoteCall @Inject() (remoteApi: RemoteApi) {
  def call(): String = remoteApi.remoteCall()
}

class RealRemoteApi extends RemoteApi {
  override def remoteCall(): String = "Real remote api call"
}

class MockRemoteApi extends RemoteApi {
  override def remoteCall(): String = "Mock remote api call"
}

次に、RemoteApiをモックする方法を示します。

7.1. Guiceを使用する

本番モジュールでは、RealRemoteApiをバインドします。

bind(classOf[RemoteApi])
  .toInstance(new RealRemoteApi)

モックモジュールでは、MockRemoteApiをバインドします。

bind(classOf[RemoteApi])
  .toInstance(new MockRemoteApi)

最後に、モックを使用してテストケースを作成できます。

"ServiceWithRemoteCall call" should {
  "invoke mock when remote api is mocked" in {
    val application = new GuiceApplicationBuilder()
      .overrides(new MockApiModule, new ServiceModule)
      .build()
    new App(application) {
      val srv = app.injector.instanceOf[ServiceWithRemoteCall]
      assert(srv.call() == "Mock remote api call")
    }
  }

  "invoke real method when real api is wired" in {
    val application = new GuiceApplicationBuilder()
      .overrides(new ApiModule, new ServiceModule)
      .build()
    new App(application) {
      val srv = app.injector.instanceOf[ServiceWithRemoteCall]
      assert(srv.call() == "Real remote api call")
    }
  }

}

7.2. MacWireの使用

同様に、同じサービスとリモートAPIを使用します。 MacWireコンポーネントの特性は、次のように記述できます。

trait ApiComponents {
  lazy val remoteApi: RemoteApi = new RealRemoteApi
}

trait MockApiComponents extends ApiComponents {
  override lazy val remoteApi: RemoteApi = new MockRemoteApi
}

次に、テストスイートでは、特性をフィクスチャとして使用します。

"ServiceWithRemoteCall call" should {

  "invoke mock when remote api is mocked" in new ServiceComponents with MockApiComponents {
    assert(serviceWithRemoteCall.call() == "Mock remote api call")
  }

  "invoke real method when real api is wired" in new ServiceComponents with ApiComponents {
    assert(serviceWithRemoteCall.call() == "Real remote api call")
  }
}

8. 結論

この記事では、Play依存性注入機能の最も一般的なユースケースのいくつかを紹介しました。 Playに付属するデフォルトの依存性注入であるGoogleGuiceに加えて、MacWireコンパイル時の依存性注入の代替手段を示しました。 いつものように、上記の例のコードはGitHubから入手できます。