友人から「しんぺいさん 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
への依存性をひっぺがせません。なぜでしょうか、依存性がクラスの中に直接書かれてしまっているからです。「FortunePostingMachine
は TwitterClient
に依存してるよ」という情報が、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
型のクラスのインスタンスを生成していないFortunePostingMachine
はTwitterClient
型に依存しているけど、具体的なクラスには依存していない
という状態を作ることができました。つまり、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 の違いについては書く気がおこったら書くかもしれないです。