おじゃまぷよ系エンジニアメモ

アプリエンジニアからサーバーとインフラエンジニアに転身しました

PHPで学ぶChain of Responsibilityパターン

この記事はPHPで学ぶデザインパターン Advent Calendar 2018の4日目の記事です。

qiita.com

Chain of Responsibilityパターンとは

よく言われるのがたらい回しパターンですね。
自分では処理できない場合次のオブジェクトに処理をお願いするパターンです。

サンプルコード

とりあえずサンプルコードです。
RPGを想定して、勇者がモンスターと戦いますがそのモンスターのレベルが高かったり強い種族の場合は自分より強い勇者に倒すのをお願いするものを想定しています。

/**
 * モンスターです。名前とレベルだけ持ってます
 */
class Monster
{
    private $name;
    private $level;

    public function getName()
    {
        return $this->name;
    }

    public function getLevel()
    {
        return $this->level;
    }

    public function __construct($name, $level)
    {
        $this->name = $name;
        $this->level = $level;
    }
}
/**
 * 抽象的な勇者です。
 * canDefeat()で倒せるかどうかの判定は具体的なサブクラスに自由に決めてもらいます
 */
abstract class Braver
{
    private $name;

    /**
     * @var Braver
     */
    private $next;

    public function __construct(String $name)
    {
        $this->name = $name;
    }

    abstract function canDefeat(Monster $monster);

    /**
     * たらい回し先をセットします
     */
    public function setNext(Braver $next)
    {
        $this->next = $next;
        return $this->next;
    }

    /**
     * モンスターと戦います。
     * 自分が倒せない相手だと次の勇者に任せます
     */
    public function fight(Monster $monster)
    {
        if ($this->canDefeat($monster)) {
            echo $this->name . " が " . $monster->getName() . " を倒した\n";
        } else if ($this->next != null) {
            $this->next->fight($monster);
        } else {
            echo "全滅した\n";
        }
    }
}

3種類の勇者を用意しました。

/**
 * 新米勇者
 */
class NewcomerBraver extends Braver
{

    /**
     * レベル10未満のスライムなら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getName() == 'スライム' && $monster->getLevel() < 10;
    }
}

/**
 * 中級勇者
 */
class IntermediateBraver extends Braver
{

    /**
     * どんなモンスターもレベル15未満なら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getLevel() < 30;
    }
}

/**
 * ベテラン勇者
 */
class ExpertBraver extends Braver
{
    /**
     * どんなモンスターもレベル50未満なら倒せる
     */
    function canDefeat(Monster $monster)
    {
        return $monster->getLevel() < 50;
    }

}
$newComerBraver = new NewcomerBraver("新米勇者");
$intermediateBraver = new IntermediateBraver("中級勇者");
$expertBraver = new ExpertBraver("ベテラン勇者");
//新米勇者->中級勇者->ベテラン勇者の順にたらい回しする
$newComerBraver->setNext($intermediateBraver)->setNext($expertBraver);

$newComerBraver->fight(new Monster("スライム", 9));//新米勇者 が スライム を倒した
$newComerBraver->fight(new Monster("ドラキー", 9));//中級勇者 が ドラキー を倒した
$newComerBraver->fight(new Monster("スライム", 20));//中級勇者 が スライム を倒した
$newComerBraver->fight(new Monster("スライム", 40));//ベテラン勇者 が スライム を倒した
$newComerBraver->fight(new Monster("りゅうおう", 99));//全滅した

どういう時につかうの?

複雑なif-elseやswitch文を見た時に、これはポリモーフィズムで解決できないか?と少し立ち止まってみます。 今回のサンプルを愚直に実装してみると(正確にはちょっと違うけど…)

$monster = new Monster("スライム", 10);
$newComerBraver = new NewcomerBraver("新米勇者");
$intermediateBraver = new IntermediateBraver("中級勇者");
$expertBraver = new ExpertBraver("ベテラン勇者");

if ($monster->getName() == 'スライム' && $monster->getLevel() < 10) {
    $newComerBraver->fight($monster);
} else if ($monster->getLevel() < 30) {
    $intermediateBraver->fight($monster);
} else if ($monster->getLevel() < 50) {
    $expertBraver->fight($monster);
} else {
    echo "全滅した";
}

だいたいこのような感じになります。
これがぐらいなら愚直に実装した方がわかりやすいかもしれませんがもう少し条件が複雑になったり if-elseのパターンが追加されると見通しが悪くなります。そして

 if ($monster->getLevel() < 50) {
    $newComerBraver->fight($monster);
} 

みたいに条件と内部の処理が一致しないミスなんかも起こるかもしれません。 CORの良いところの1つに、条件と処理がひとつのクラスにまとまっているところですね。

もう少しシンプルに

CORをデザインパターンそのまま組むのはちょっとしんどいのでもう少しシンプルにした形で
配列に入れてforeachで回して書くCORパターンもどきを好んで使います。
これだと条件が増えても新たにBraverをextendsしたクラスを用意して配列にaddするだけでいいので楽です。

$braverParty = [
    new NewcomerBraver("新米勇者"),
    new IntermediateBraver("中級勇者"),
    new ExpertBraver("ベテラン勇者")];

$monster = new Monster("スライム", 10);
foreach ($braverParty as $braver) {
    if ($braver->canDefeat($monster)) {
        $braver->fight($monster);
        return;
    }
}
echo "全滅した";

かなりざっくりした説明になりましたが、if-elseやswitch文を見た時にポリモーフィズムでシンプルに書きたいという欲求に駆られたときに思い出してみるのもいいかもしれませんね。

参考資料

www.dezapatan.com