既存コードに対し、PHPUnitでテストコードを書く際に問題となる下記のようなクラス。
良いサンプルクラスを思いつかなかったのでデザインについてはあまり考えないでほしい。
<?php
class Beer
{
public function open()
{
$opener = new BottleScrew();
if ($opener->pull()) {
return 'success';
}
return 'open';
}
}
class BottleScrew
{
public function pull()
{
return true;
}
}
$beer = new Beer();
$beer->open();
これ(Beer::open)に対しテストコードを書く時、どうすれば良いか。
-このクラス(メソッド)の開発者を弾劾しチームに対しテストコードを書けない責任は自分にはないことを示した上で諦める
-@codeCoverageIgnoreアノテーションを用いなかったことにする
-DI(依存性の注入)する
パッと思いつくのはこの3つの方法。
クラスを作成した当人がいれば真っ先に一つ目の方法を選ぶが、そうでもない時は基本的に三つ目を選択するのが良いのだろう。
というわけで、PHPでのDIについて紹介する。
#DI(依存性の注入)とは
クラス間の依存性を解消するための方法。
密結合から疎結合に。
簡単にいえば、「外でインスタンスを生成してから、中に入れろ」という話。
#3つのDI
Wikipediaで「DIの種類」として下記3つが紹介されていたので(2016/03/21時点)、それぞれを冒頭の例で紹介する。
※どうしても、ヒンティングをBottleScrewにする気にならなかったのでオープナーインターフェースを用意。
##インターフェース注入
<?php
class Beer
{
private $_opener;
public function open(Opener $opener)
{
if ($opener->pull()) {
return 'success';
}
return 'fail';
}
}
interface Opener
{
public function pull();
}
class BottleScrew implements Opener
{
public function pull()
{
return true;
}
}
$beer = new Beer();
echo $beer->open(new BottleScrew()) . PHP_EOL;
##setter注入
<?php
class Beer
{
private $_opener;
public function open()
{
if ($this->_opener->pull()) {
return 'success';
}
return 'fail';
}
public function setOpener(Opener $opener)
{
$this->_opener = $opener;
}
}
interface Opener
{
public function pull();
}
class BottleScrew implements Opener
{
public function pull()
{
return true;
}
}
$beer = new Beer();
$beer->setOpener(new BottleScrew());
echo $beer->open() . PHP_EOL;
##コンストラクタ注入
<?php
class Beer
{
private $_opener;
public function __construct(Opener $opener)
{
$this->_opener = $opener;
}
public function open()
{
if ($this->_opener->pull()) {
return 'success';
}
return 'fail';
}
}
interface Opener
{
public function pull();
}
class BottleScrew
{
public function pull()
{
return true;
}
}
$beer = new Beer(new BottleScrew());
echo $beer->open() . PHP_EOL;
#DIを行うメリット
テストコードが書ける以外にもDIを行うメリットはある。
(というか、密結合状態を平然と生み出すことは問題であって、その改善をメリットと呼ぶのもどうかと思うが……)
例えば、このビールを「栓抜き」ではなく、「歯」で開けれるようにもなる。
ポリリズ……ポリモフィズムですね。
<?php
class Beer
{
private $_opener;
public function __construct(Opener $opener)
{
$this->_opener = $opener;
}
public function open()
{
if ($this->_opener->pull()) {
return 'success';
}
return 'fail';
}
}
interface Opener
{
public function pull();
}
class Tooth implements Opener
{
public function pull()
{
return false;
}
}
$beer = new Beer(new Tooth());
echo $beer->open() . PHP_EOL;
いや、歯では開かないよな。