依存性注入(DI)について勉強する
少し前にPHPUnitを触る機会があったのですが、依存性注入(Dependency Injection:以下「DI」)について考慮されていないプログラムだったため、非常にテストコードの記述が大変だったコトがありました。
また、何度か記事にさせていただいているPhalconにもDIコンテナが用意されているものの、イマイチ理解していなかったコトもあったりしたため、DIについてちょっと調べてみることにしてみました。
まだ理解が浅く、見当違いのコトを書いてしまっているかもしれませんが、そういった箇所がありましたら遠慮なく突っ込んで頂ければと思います。
DIとは
DIを利用したプログラムを作成する場合、コンポーネント間の関係はインタフェースを用いて記述し、具体的なコンポーネントを指定しない。具体的にどのコンポーネントを利用するかは別のコンポーネントや外部ファイル等を利用することで、コンポーネント間の依存関係を薄くすることができる。
ということだそうですが、やたらとコンポーネント推しされてるのでPHP的にどうすれば良いかと考えた時に乱暴に答えると、「メソッド内でnewしない」という考え方になるかと思います。
例えば、
<?php class Foo { public function fooFunction() { $bar = new Bar(); if( $bar->barFunction() ) { return false; } return true; } }
という形ではなく、
<?php class Foo { public function fooFunction( Bar $bar ) { if( $bar->barFunction() ) { return false; } return true; } }
という書き方になる感じですね。
では、上の例では何が駄目なのでしょう?
DIのメリット
DIを考慮すると下記のようなメリットが生まれるようです。
ココでは、「単体テストの効率化」に目を向けて考えてみます。
先ほどの例でPHPUnitのテストを書く場合にどうなるか考えてみましょう。
<?php class FooTest extends PHPUnit_Framework_TestCase { public function testFooFunction() { $foo = new Foo(); $this->assertTrue( $foo->fooFunction() ); // または $this->assertFalse( $foo->fooFunction() ); } }
この様に、Foo
クラスだけで解決しようにもBar
クラスに依存しているため、単体テストが行えない状態になっています。
(barFunction
の中の処理にもよりますが、そちらの値も変更しないと行けないのであれば結合テストになってしまうかと思います。)
では、下の例ではどうでしょう?
コチラの処理を行う際には「モック化」を利用することで行うことが出来ます。
(モック化については詳しく記載はしませんが、このあたりの記事が参考になるかと思います)
<?php class FooTest extends PHPUnit_Framework_TestCase { public function testFooFunction() { /** * trueを返すBarクラスのモックを作る */ $bar = $this->getMock( 'Bar' ); $bar->expects( $this->any() ) ->method( 'barFunction' ) ->will( $this->returnValue( true ) ); $foo = new Foo(); $this->assertTrue( $foo->fooFunction( $bar ) ); } }
この様な形で、Bar
クラスに依存すること無く、Foo
クラスのテストを行うことが出来ます。
それでは、今度はもう少し注入する方法を考えてみましょう。
抽象に依存させる
先ほどの例では、Bar
クラスを引数に渡していましたが、例えばDBのクラス等は様々なDBと接続するためにクラスが分割されているコトがあるかと思います。
そういった場合には、Bar
クラスに限定されていると不便に感じてしまいますよね。
そこで、インターフェイスを引数に渡すことで、曖昧な依存関係を持たせることが出来ます。
<?php class Foo { public function fooFunction( BarInterface $bar ) { // 何らかの処理 } }
<?php interface BarInterface { public function barFunction(); } class Bar implements BarInterface { public function barFunction() { // 何らかの処理 } }
これで、BarInterface
を実装しているクラスであれば引数に渡せるようになりました。
つまり、上の例とは別にBarInterface
を実装したBarCode
クラスなんかも渡すことが出来ます。
その他
今回はDIのパターンと、単体テストを行う上でどういったメリットがあるかの確認と頭の整理のためにメモを残しておきました。
PHPでサンプルコードを書きましたが、他の言語でも同じ様な考え方を用いるコトも出来ますね。
また、単体テストについてを重きに置いて書いていましたが、結合度が下がる上でのメリットも沢山あるかと思います。(例えば、依存しているクラスが出来上がっていなくてもテストが出来る、等)
実装する上では考えることが増えてしまいますが、考慮しておいて損は無いのかなぁと思いました。
その他、前述したDIパターンをまとめたDIコンテナという考え方もあり、最近のFWはコンテナが用意されているコトも多いかと思います。次はDIコンテナの辺りも調べ、どう使っていくべきなのか知っておきたいですね。