読者です 読者をやめる 読者になる 読者になる

要するに DI って何なのという話

programming

友人から「しんぺいさん DI について書いてほしい」みたいな話をだいぶ前からされてたんだけど書く気力ずっとなかった。でも仕事の気分転換にちょっとずつ書いたやつがいい量まとまったので公開するです。たいしたことは書いてないっていうか知ってるひとにはあたりまえのことしか書いてない。サンプルコードはわたしの趣味で Scala で書いてあるが、Java が読めればなんとなく読めると思います。

DI ってなに

Dependency Injection、日本語で言えば依存性の注入です。おしまい。

で記事を終えてもいいんだけど、そもそも依存性とはなんなのか、それを注入するとはどういうことなのか、なぜ DI が必要となるのかみたいな話をこれからします。

そもそも依存性ってなあに

例を出します。入力された文字列をもとにおみくじをひいて、その結果を twitter に投稿するプログラムにしましょう。

まずは普通に書いてみましょう。

object Fortune{
  def main(args: Array[String]) = {
    val input = args.head
    val machine = new FortunePostingMachine()
    machine.run(input)
  }
}
class FortunePostingMachine {
  private val fortunes: Seq[String] = IndexedSeq("大吉", "中吉", "小吉", "凶", "大凶")
  private val twitterClient: TwitterClient = new TwitterClient(/*snip*/) //...(1)

  def run(input: String): Unit = {
    val fortune = getFortune(input)
    
    twitterClient.updateStatus(fortune)
  }

  private def getFortune(input: String): String = {
    val index = hash(input) % fortunes.length
    fortunes(index)
  }

  private def hash(input: String): Int = {
    val hashing = scala.util.hashing.Hashing.default
    hashing.hash(input)
  }
}
class TwitterClient(/*snip*/){
  
  def updateStatus(status: String): Unit = {
    /* 実際に Twitter に投稿する処理 */
  }
}

さて、プログラムのエントリポイントである Fortune オブジェクトの他に、FortunePostingMachine というクラスと TwitterClient というクラスがあります。TwitterClient クラスはちょっと実装が複雑になるので中身は書いてありません。

ここで、FortunePostingMachineに注目してみましょう。実際におみくじを引いて、twitter に結果を post してくれる君ですね。その中でも、(1)で示した部分に注目してみましょう。FortunePostingMachineコンストラクタ内で、TwitterClient クラスをインスタンス化しています。ということは、FortunePostingMachineコンパイルするためには(あるいは正しく動かすためには)、TwitterClientというクラスが正しく動く状態ですでに定義されている必要がありますね。

こういう、「クラスAをコンパイルするためには(あるいは正しく動かすためには)、クラスBが既に出来上がってないとだめ」という状態のことを、「クラスAはクラスBに依存している」とか言いますよね。こういう、「どのクラスがどのクラスに依存しているのかという情報」のことを依存性と呼びます。今は、(1)の行に依存性がベタ書きされている状態ですね。

依存性がベタ書きされていると何がまずいのか

いろんなケースが考えられます。まず、単体テストのしにくさが挙げられるでしょう。このFortunePostingMachineに対するテストを走らせるたびに、毎回実際にTwitterに投稿されるのはちょっとまずいですよね。なんかこう、うまいことモックしたりスタブしたりしたいなって思うはずです。

あるいは、もしかしたら開発バージョンでは実際のTwitterに投稿したくない、みたいな話もあるかもしれません。そういうときにも、ちょっと困っちゃいますね。

あと、このままだと、仮にTwitterClientが壊れちゃったときに、このクラスのテストも一緒にこけちゃいます。そこだけ見ると、このクラスが悪かったのかそれともTwitterClientが悪かったのかわからなくて困っちゃいますね。

このような問題から、「依存性をひっぺがしたいなぁ」という発想が生まれるのは自然だと言えそうです。しかし、このままではFortunePostingMachineからTwitterClientへの依存性をひっぺがせません。なぜでしょうか、依存性がクラスの中に直接書かれてしまっているからです。「FortunePostingMachineTwitterClient に依存してるよ」という情報が、FortunePostingMachine 自身の内部に書かれているせいで、この依存性を外からひっぺがすことができないのです。

そこで、じゃあ「依存性をクラスの外で定義すればいいんじゃないの?」という発想が生まれてくるわけです。大事なことなのでもう一度言葉を変えて言います。「クラスAにはクラスBが必要だよっていう情報がクラスA自身の中で定義されてると、依存性をひっぺがせなくて困るよね。じゃあ、AがBに依存しているよっていう情報を、クラスAの外で定義して外からクラスAのインスタンスに突っ込んであげたらいいんじゃないの」ということです。もっと簡単に言うと「依存性を外から注入したいね」ってことです。かんたんに言ってしまうと、DIってのはこういう発想のことです。

DI の例

じゃあ、実際にFortunePostingMachineからTwitterClientへの依存性をひっぺがしてみましょう。単純に考えると、「TwitterClientインスタンスFortunePostingMachineの中でインスタンス化してるのがまずいなら、FortunePostingMachineの外でインスタンス化して、それを渡してあげればよくね?」という発想が出てきます。コードはこんな感じ。

object Fortune{
  def main(args: Array[String]) = {
    val input = args.head

    val twitterClient = new TwitterClient(/*snip*/) //...(1)
    val machine = new FortunePostingMachine(twitterClient)
    machine.run(input)
  }
}
class FortunePostingMachine(twitterClient: TwitterClient) { //...(2)
  private val fortunes: Seq[String] = IndexedSeq("大吉", "中吉", "小吉", "凶", "大凶")

  def run(input: String): Unit = {
    val fortune = getFortune(input)

    twitterClient.updateStatus(fortune) //...(3)
  }

  private def getFortune(input: String): String = {
    val index = hash(input) % fortunes.length
    fortunes(index)
  }

  private def hash(input: String): Int = {
    val hashing = scala.util.hashing.Hashing.default[String]
    hashing.hash(input)
  }
}

(2)に注目してください。FortunePostingMachineクラスのコンストラクタで twitterClient を受け取るように変更したことで、実際のインスタンス化を外(1)に出すことができました。

しかしこれだけではマズいですね。(2)ではまだ「TwitterClient」という型が使われてますし、(3) でも実際に twitterClient を利用しています。まだ FortunePostingMachine は「TwitterClient クラスがないとコンパイルできないし正しく動かない」という状態です。つまり、まだTwitterClientへの依存性を断ち切ることができていません。

そこで、今度はTwitterClientクラスのほうをちょっといじりましょう。

trait TwitterClient{
  def updateStatus(input: String): Unit
}

class TwitterClientImpl(/* snip */) extends TwitterClient {
  def updateStatus(status: String) = {
    /*snip*/
  }
}

TwitterClientという型自体は、classではなくてtrait(javaで言うinterfaceみたいなもん)として、実際の実装は TwitterClientImpl のほうに移します(説明のためとは言え、えらくジャバジャバしいコードであるな……)。と、TwitterClientが class ではなくなったので、今まで TwitterClientインスタンス化していた部分も書き換えないとですね。

object Fortune{
  def main(args: Array[String]) = {
    val input = args.head

    val twitterClient: TwitterClient = new TwitterClientImpl(/*snip*/) //...(1)
    val machine = new FortunePostingMachine(twitterClient)
    machine.run(input)
  }
}

さて、これで、

  • FortunePostingMachineの中でTwitterClient型のクラスのインスタンスを生成していない
  • FortunePostingMachineTwitterClient型に依存しているけど、具体的なクラスには依存していない

という状態を作ることができました。つまり、FortunePostingMachineに、外から依存性を注入してあげることができました!。

これで、TwitterClientImplがまだ作られてなかったとしても、たとえば単体テストのときには TwitterClient を継承した MockedTwitterClient みたいなものを作って、そのインスタンスFortunePostingMachine に渡すようにしてあげれば、コンパイルもできるし単体テストもできます。さらに、仮にTwitterClientImplがぶっこわれても、それにつられてFortunePostingMachineのテストが落ちるなんてこともなくせそうです。

さて、こんな単純な例でも、外から依存性を注入できている以上、立派な DI です。

その他の手法

DI にはいろいろな方法があって、さっきはコンストラクタで依存対象のオブジェクトを渡したけど、同じような感じでたとえばセッターでオブジェクトを渡すことでも依存性を外から注入できます(DIコンテナがこの形式を要求しない限り取るべきではない手法だとわたしは思っていますが)。あるいは、より Scala らしく書くならこういうのはどうでしょうか。

trait FortunePostingMachine {
  private val fortunes: Seq[String] = IndexedSeq("大吉", "中吉", "小吉", "凶", "大凶")
  protected val twitterClient: TwitterClient

  def run(input: String): Unit = {
    val fortune = getFortune(input)

    twitterClient.updateStatus(fortune)
  }

  private def getFortune(input: String): String = {
    val index = hash(input) % fortunes.length
    fortunes(index)
  }

  private def hash(input: String): Int = {
    val hashing = scala.util.hashing.Hashing.default[String]
    hashing.hash(input)
  }
}
trait TwitterClient{
  def updateStatus(input: String): Unit
}

object TwitterClient {
  def default(/*snip*/) = new Default(/*snip*/)

  class Default(/* snip */) extends TwitterClient {
    def updateStatus(status: String): Unit = {
      /*snip*/
    }
  }
}
object Fortune{
  def main(args: Array[String]) = {
    val input = args.head

    val machine = new FortunePostingMachine {
      protected val twitterClient = TwitterClient.default(/*snip*/)
    }
    machine.run(input)
  }
}

FortunePostingMachine を trait にして、そこには「TwitterClient 型のプロパティを持ってるよ」という情報だけを書いておきます。newするときにその場実装で実際のtwitterClientを渡してあげるというパターンですね。なにげに TwitterClient のほうも Impl とかじゃなくて TwitterClient オブジェクトの中に そのデフォルト実装である Default クラスを定義して、それを TwitterClient.default で取得できるようにしてみました。Java だったら TwitterClient を Abstract Class にしておけば似たようなことが実現できそうですね。

これでも DI が実現できています。

まとめ

依存性ってなんなの

「クラスA を動かすためにはクラスBが必要」っていう情報のこと

依存性の注入ってなんなの

「クラスA を動かすためにはクラスBが必要」っていう情報をクラスAの中に書かないで外に書いて外から注入できるようにすること

依存性を注入できるとなにがうれしいの

クラスBがなくてもクラスAを動かせるようになる。粗結合になるしテストしやすくなるしうれしい。

そのやり方

いろいろあるし言語の文化とかによって好まれる手法は異なる。今回はコンストラクタで依存性を注入するパターンと、注入対象を trait にしておいて new するときにその場実装することで依存性を注入するパターンを見てみた。

言い残したこととか

さて、これで、「DI ってなんなの」「なんで必要なの」「どうやるの」みたいな話については一通り見れたのではないでしょうか。で、話は「じゃあDIコンテナってなんなの」みたいなところにつながって行くのですが、長くなったので続きはまた今度。っていうか多分書かないと思う。ここまでわかったら「DIコンテナってのは、依存性を注入される側のクラスを規約通りに書いておくと、設定したとおりに DI してくれる君です」で通じる気がするし、柔軟に書ける LL系言語とか Scala とかならそもそも DI コンテナなんて必要ないという話もあるし。

あと、なんでもかんでも DI すればいいってわけでもなくて、必要ない DI は設計過剰みたいな感じでわたしは好みません。DI が必要だなーって感じたとき(つまり依存情報をクラス内部からひっぺがしたいなーって思ったとき)に DI するようにすればいいわけで、とにかくDI!かならずDI!いつでもDI!盲目DI!みたいなのは違うと思う(実際上記サンプルでは scala.util.hashing.Hashing.Defaultへの依存はベタ書きしている。なぜならこの依存をひっぺがす必要性を感じないから)。

あっ、Dependency Injection と Dependency Lookup の違いについては書く気がおこったら書くかもしれないです。