もしこの世にDIコンテナがなかったら。

この記事はただの集団 Advent Calendar 2019の9日目の記事です。

ネット上に非常に多くの記事があるので、DIコンテナの話はほとんどしません。

この記事では、DIコンテナの目的である、

オブジェクトの生成と利用の分離

制御の反転

の話をしますmm

もしこの世にDIコンテナが存在していなくて、DIコンテナが行なっていることを手動でしなければいけない場合、どのような実装が考えられるのか、実際にコードを書いてみます。

前提知識

DIコンテナとは

アプリケーションにDI(Dependency Injection: 依存性注入)機能を提供するフレームワーク

詳しくは、以下の記事にまとめられているので参照してください。

DIコンテナとは DI・DIコンテナ、ちゃんと理解出来てる・・? -Qiita

何をしてくれるのか

そもそもDIはなぜ必要なのでしょうか? DIの非常に大事な目的として、以下の目的があります。

1. オブジェクトの生成と利用を分離する

これは、関心ごとの分離と言うこともできます。

2. 制御の反転

SOLIDで言うと、依存性逆転の原則です。

これらの目的を実現するための1つの方法としてDIがあります。

DI以外にもこれらを実現するものとして、様々な方法があります。

この記事では、上記の目的を実現する様々な方法の中のいくつかを実際にコードベースで紹介していきます。

「オブジェクトの生成と利用を分離」しないと何がだめ?

1. 単一責任の分離ができていないので、テストが非常にしにくくなる。

2. オブジェクトの生成ロジックがそこらへんに散らばってしまい、時に著しい重複を生み出してしまうことがある。

searchメソッドをテストすると、実際にJobRepositoryImpleWithMysqlインスタンスが作られてしまうので、 もし、JobRepositoryImpleWithMysqlがDBとの連携を行なっていた場合に、非常にテストがやりずらい。

class SearchUseCase {
  def search = {
    val repositoryImpleWithMysql = new JobRepositoryImpleWithMysql
    ...........
  }
}

「制御の反転」しないと何がだめ?

1. 具象クラスに依存することで、具象クラスが変更されるとごっそり修正が必要になる。

2. 具象クラスがDBとのコネクションを行なっている場合など、テストがしにくい。

JobRepositoryImpleWithMysqlをjobRepositoryImpleWithElasticSearchに変更すると、 利用側(SearchUseCase)でも、修正が必要になってしまう。


class SearchUseCase {
  def search = {
    val repositoryImpleWithMysql = new JobRepositoryImpleWithMysql
    ...........
  }
}


class JobRepositoryImpleWithMysql {
  def getJobs: Seq[String] = Seq("job1", "job2")
}

「オブジェクトの生成と利用を分離」を実現するパターン

大きく分けて3つのパターンがあります。 この中で、太文字のものを実際のコードで紹介していきます。

  • 依存性注入(DI)
    • コンストラクタパターン
    • cake pattern
  • ファクトリ
    • abstract factory pattern
  • mainの分離

cake patternabstract factory patternは「制御の反転」も実現しているので、まずコンストラクタパターンのみ見ていきます。

cake patternは、scala特有のパターンらしい。(参照

コンストラクタパターン

package study.ConstractPattern

class Main {

  def main(args: Array[String]): Unit = {
    val useCase = new SearchUseCase(new JobRepositoryImpleWithMysql)
    useCase.search
  }
}

class SearchUseCase(jobRepositoryImpleWithMysql: JobRepositoryImpleWithMysql) {
  // コンストラクタ引数で、SearchUseCaseにJobRepositoryImpleを委譲している。
  // searchUseCaseないでnew JobRepositoryImpleして、オブジェクトを注入するのではなく、
  // SearchUseCaseオブジェクトを宣言している箇所から、注入することによって
  // 外部から依存性注入をすることができている。
  // 外部から、これ使えって!オブジェクトを注入している。
  
  val jobRepositoryImpleInstance = jobRepositoryImpleWithMysql

  def search: Seq[String] = {
    jobRepositoryImpleInstance.getJobs
  }
}

trait JobRepository {
  def getJobs: Seq[String]
}

class JobRepositoryImpleWithMysql extends JobRepository {
  override def getJobs: Seq[String] = Seq("job1", "job2")
}

class jobRepositoryImpleWithElasticSearch extends JobRepository {
  override def getJobs: Seq[String] = Seq("job1", "job2")
}

コメントでも書いている通り、コンストラクタ引数として利用するオブジェクトを注入することによって、オブジェクトの生成と利用を分離することができています。

できること

  • オブジェクトの構成ロジックと、通常の実行処理を分離することによって、単一責任の法則を守れている。
  • なので、テストがしやすくなる

できないこと

  • 結局、アプリケーション実行タイミングで、newしてオブジェクトを作成しないといけないので、そこに責務が集中する。
  • 具象クラスがuseCase側に見えてしまっている(直接、具象クラスを呼び出している)ので、具象クラスの実装が変わると、useCase側にも影響が及んでしまう。(これだけでは、制御の反転はできていない)

ex) jobRepositoryImpleWithMysqlが、jobRepositoryImpleWithElasticSearchに変わると、ごっそり変更を加えないといけなくなる。

制御の反転を実現するパターン

以下の方法があります。

  • 依存性注入(DI)
    • cake pattern
  • ファクトリ
    • abstract factory pattern

abstract factory pattern

package study.abstractFactoryPattern

class Main {
  def main(args: Array[String]): Unit = {
    val searchUseCase = new SearchUseCase
    searchUseCase.search
  }
}


// 使用側は、具象クラスについて知る必要がなくなる。
// アプリケーション側でオブジェクトの生成に関する制御を行う

class SearchUseCase {
  def search = {
    val repository: JobRepository = JobRepositoryFactory.createJobRepository
    repository.getJobs
  }
}

// abstract factory
trait AbstractFactory {
  def createJobRepository: JobRepository
}

// abstract product
trait JobRepository {
  def getJobs: Seq[String]
}

// concrete factory
object JobRepositoryFactory extends AbstractFactory {
  override def createJobRepository: JobRepository = new JobRepositoryImple
}

// concrete product
class JobRepositoryImple extends JobRepository {
  override def getJobs: Seq[String] = Seq("job1", "job2")
}

できること

  • factory.createJobRepositoryで、オブジェクト生成部分を実行処理を分離することで、関心ごとの分離を実現できている。
  • 以下の例のように、永続化層を呼び出す側は、具象クラスを知る必要がなくなる。
    • repository.getJobsなので、呼び出し側は具象クラスが変更されても(例えば、JobRepositoryImpleWithMysql -> JobRepositoryImpleWithElasticSearch) 影響を受けない。

cake pattern

package study.cakePattern

class Main {
  def main(args: Array[String]): Unit = {
    // searchUseCase自体をインジェクトしている
    val useCase = ComponentRegistry.searchUseCase
    useCase.search
  }
}


// 抽象のコンポーネント
// componentで囲って、それぞれ名前空間を作ってあげる
trait JobRepositoryComponent {
  val jobRepository: JobRepository

  trait JobRepository {
    def search: Unit
  }
}

// 具象のコンポーネント
// componentで囲って、それぞれ名前空間を作ってあげる
trait JobRepositoryComponentImple extends JobRepositoryComponent {
  class JobRepositoryImple extends JobRepository {
    override def search: Unit = println("create user")
  }
}

// クライアントのコンポーネント
// 自分型アノテーションを使って、UserRepositoryへの依存性を宣言する
trait SearchUseCaseComponent { self: JobRepositoryComponent =>
  class SearchUseCase {
    def search = {
      // このjobRepositoryがインジェクトされたい
      self.jobRepository.search
    }
  }
}

// インジェクター(DIコンテナの役割)
// SearchUseCaseComponentは、自分型アノテーションでJobRepositoryComponentを宣言しているので
// JobRepositoryComponentImpleもmixinしてあげないといけない
object ComponentRegistry extends SearchUseCaseComponent with JobRepositoryComponentImple {
  override val jobRepository = new JobRepositoryImple
  val searchUseCase = new SearchUseCase
}

できること

SearchUseCaseクラスのsearchメソッドの中で、具象クラス(JobRepositoryImple)に依存するのではなく、抽象クラス(JobRepository)に依存することで、制御の反転が実現できています。

まとめ

以上のようにいくつかのやり方を見ていきました。 DIコンテナを使うことで、内部的に複雑な処理を行なってくれるのでよりDIコンテナの便利さを実感しました。

DIコンテナを導入するデメリットはあるのかな?

最後までありがとうございましたmm

参考記事

依存性の注入 - Wikipedia

沈思黙考:デザインパターン(Abstract Factory パターン) - Qiita

実戦での Scala: Cake パターンを用いた Dependency Injection (DI) | eed3si9n

ScalaでDI (Cake Pattern 導入編) | TECHSCORE BLOG