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

はむはむエンジニアぶろぐ

このブログのコンセプトは"ハッキングの為なら愛する家族を傷つけることをいとわない" 自分にとってエンジニアリングは "手段ではなく生きる目的" である

PHPでDIをする

GoF リファクタリング PHPUnit テスト Phake PHP DI オブジェクト指向

f:id:secret_hamuhamu:20150816222337j:plain
PHPでDI( Dependency Injection )をします。
DIとは、日本語訳で依存性の注入です。

依存性というのは、クラスから別のクラスを呼び出している状態です。
例えば、犬が鳴いたら猫も鳴く。

<?php
class Dog
{
    public function barks()
    {
        $cat = new Cat();
        $cat->meow();
    }
}

Dogクラスのbarksメソッドは、Catクラスに依存しています。
こういったようにクラス内部で別のクラスを new しているものを Dependency Lookup といいます。

こういった依存をしている場合、テスト実践者ならお気づきだと思いますがテストが大変です。
Catクラスが出来上がってないと、テストできないしCatクラスがDBやネットワークを介するクラスであれば難易度が上昇します。

テストできるように依存しているクラスにモックを差し込める穴を開けようというのが、 Dependency Injection です。
DIを使ったサンプルコードとテストを書いてみました。

依存性について

基本的に依存の向きが一方向であれば問題無いです。
依存というのは、避けて通れない道で、できるだけ依存関係を排除するのが良い設計と言われています。

DIのメリット

DIは、依存している箇所を外から注入しようというものです。

メリットとしてこのようなものがあります。

  • テストが容易になる
  • DIを行うクラスは、依存対象のクラスが完成していなくても開発できるので開発効率が増す
  • DBやネットワークといった外部環境に強く依存する処理を切り離せる

etc...他にもあると思います。

DIの種類

DIには、やり方が3種類くらいあります。
他にもあるんですけれども(コンテナと呼ばれるもの)ライブラリいらずにすぐに出来るのは、この3つです。

  • メソッド・インジェクション
  • セッタ・インジェクション
  • コンストラクタ・インジェクション

順番にやっていきます。

メソッド・インジェクション

メソッドの引数に依存対象のクラスを渡す。

<?php
class Dog
{
    public function barks(Cat $cat)
    {
        $cat->meow();
    }
}

$cat = new Cat();
$dog = new Dog();
$dog->barks($cat);

セッタ・インジェクション

DI用のセッタを用意し依存対象のクラスを渡す。

<?php
class Dog
{
    private $cat;
    public function setCat(Cat $cat)
    {
        $this->cat = $cat;
    }
    public function barks()
    {
        $this->cat->meow();
    }
}

$cat = new Cat();
$dog = new Dog();
$dog->setCat($cat);
$dog->barks();

コンストラクタ・インジェクション

コンストラクタに依存対象のクラスを渡す。

<?php
class Dog
{
    private $cat;
    public function __construct(Cat $cat)
    {
        $this->cat = $cat;
    }
    public function barks()
    {
        $this->cat->meow();
    }
}

$cat = new Cat();
$dog = new Dog($cat);
$dog->barks();

どれがいいとは一概には言えません。
好みもありますが、私はコンストラクタ・インジェクションが好きです。

ただ、DIするのはいいんですけれども、呼び出し側が大変になります。

DI前

$dog = new Dog();
$dog->barks();

DI後(コンストラクタ・インジェクション)

$cat = new Cat();
$dog = new Dog($cat);
$dog->barks();

今まで、Dogクラスに閉じていたCatクラスを外から渡すようにしたので、Catのインスタンスを生成する必要性が出てきました。
これが、5クラスとかなってくると使うのが大変なのは容易に想像できる。
それに複数箇所で、Dogクラスが使われていて依存クラスのクラス名が変更されたりインターフェースが変更されると、リファクタリングが大変である。

この問題は、Factoryメソッドを使うことで解決できる。

Factoryメソッドを用いたDI

複数のクラスに依存しているFacadeクラスを用いてDIを行おうと思う。
Facadeクラスがどういうものかは、こちら を参照ください。

DI前

<?php
class BuyFacade
{
    public function execute($userId, $food, $num, $money)
    {
        try {
            $calcer = new Calcer();                 // 購入金額計算クラス
            $payment = $calcer->calc($food, $num);  // 購入金額

            $resistor = new Resistor();
            $resistor->pay($payment, $money);       // 商品の支払いを行う

            $coupon = new Coupon();
            $coupon->publish($userId);              // クーポンの発行を行う
        } catch (Exception $e) {
            $logger = new Logger();
            $logger->write($e->getMessage());      // なんかエラーが起こったらログを書く

            throw new Exception($e);
        }
    }
}

// 呼び出し
$facade = new BuyFacade();
$facade->execute($userId, $food, $num, $money);

execute メソッドに複数のクラスが依存していることが分かる。
このままだと、テストしづらいのでコンストラクタ・インジェクションで DI します。
そして、呼び出し側も楽になるようにFactoryメソッドを追加します。

<?php

class BuyFacadeDI
{
    private $userId;
    private $food;
    private $num;
    private $money;
    private $calcer;
    private $resistor;
    private $coupon;
    private $logger;

    public function __construct($userId, $food, $num, $money, Calcer $calcer, Resistor $resistor, Coupon $coupon, Logger $logger)
    {
        $this->userId   = $userId;
        $this->food     = $food;
        $this->num      = $num;
        $this->money    = $money;
        $this->calcer   = $calcer;
        $this->resistor = $resistor;
        $this->coupon   = $coupon;
        $this->logger   = $logger;
    }

    public static function create($userId, $food, $num, $money)
    {
        return new BuyFacadeDI(
            $userId,
            $food,
            $num,
            $money,
            new Calcer(),
            new Resistor(),
            new Coupon(),
            new Logger()
        );
    }

    public function execute()
    {
        try {
            $payment = $this->calcer->calc($this->food, $this->num);  // 購入金額
            $this->resistor->pay($payment, $this->money);             // 商品の支払いを行う
            $this->coupon->publish($this->userId);                    // クーポンの発行を行う
        } catch (Exception $e) {
            $this->logger->write($e->getMessage());                   // なんかエラーが起こったらログを書く

            throw new Exception($e);
        }
    }
}

// 呼び出し
$facadeDi = BuyFacadeDI::create($userId, $food, $num, $money);

Factoryメソッドが create メソッド。
Factoryメソッドを用いることで、呼び出し側が楽になれる。
コンストラクタは、プロダクションコードでは使用せず、テスト時に用いる。

この DI を用いた BuyFacadeDI クラスをテストしてみる。

DIを用いたクラスのテスト

テスティングツールに PHPUnit と モッキングフレームワークに Phake を用いる。

環境構成

  • PHPUnit 4.7.7
  • Phake 2.02

テストコード

<?php
require_once 'BuyFacadeDi.php';
require_once '../vendor/autoload.php';
class BuyFacadeTest extends PHPUnit_Framework_TestCase
{
    /** @test */
    public function createを用いてインスタンスを生成できること()
    {
        $userId = 'user-100';
        $food   = 'ラーメン';
        $num    = 1;
        $money  = 1000;

        $facadeDi = BuyFacadeDI::create($userId, $food, $num, $money);
    }

    /** @test */
    public function 期待通りにメソッドを呼び出せること()
    {
        $userId = 'user-100';
        $food   = 'ラーメン';
        $num    = 1;
        $money  = '1000';
        $calcer   = Phake::mock('Calcer');
        $resistor = Phake::mock('Resistor');
        $coupon   = Phake::mock('Coupon');
        $logger   = Phake::mock('Logger');

        Phake::when($calcer)->calc(/*food*/'ラーメン', /*num*/1)->thenReturn(800);
        Phake::when($resistor)->pay(/*$payment*/800, /*$money*/1000);
        Phake::when($coupon)->publish(/*$userId*/'user-100');

        $facadeDi = new BuyFacadeDI($userId, $food, $num, $money, $calcer, $resistor, $coupon, $logger);

        $facadeDi->execute();
        Phake::inOrder(
            Phake::verify($calcer,   Phake::times(1))->calc('ラーメン', 1),
            Phake::verify($resistor, Phake::times(1))->pay(800, 1000),
            Phake::verify($coupon,   Phake::times(1))->publish('user-100')
        );
    }
}

例外時に、Loggerクラスのwriteメソッドが呼ばれるテストは割愛。

Phakeを用いてモックすることで、実際のオブジェクトのインスタンスを生成しなくて済む。
このテスト(期待通りにメソッドを呼び出せること)で行いたいことは以下である。

  • メソッドを正しい引数で呼び出せているか?
  • メソッドを正しい規則で呼び出せているか?
  • タイポはないか?

LL言語は、実行時にコンパイルされるので、テストで実行してみないとタイポに気づかない。

以上。依存性を注入したテストでした。

おすすめの本

プログラマが知るべき97のこと

プログラマが知るべき97のこと

  • 作者: 和田卓人,Kevlin Henney,夏目大
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2010/12/18
  • メディア: 単行本(ソフトカバー)
  • 購入: 58人 クリック: 2,107回
  • この商品を含むブログ (342件) を見る

オブジェクト指向でなぜつくるのか 第2版

オブジェクト指向でなぜつくるのか 第2版

レガシーコード改善ガイド (Object Oriented SELECTION)

レガシーコード改善ガイド (Object Oriented SELECTION)

  • 作者: マイケル・C・フェザーズ,ウルシステムズ株式会社,平澤章,越智典子,稲葉信之,田村友彦,小堀真義
  • 出版社/メーカー: 翔泳社
  • 発売日: 2009/07/14
  • メディア: 大型本
  • 購入: 45人 クリック: 673回
  • この商品を含むブログ (152件) を見る

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)