Strategyパターンについて

に公開

はじめに

Strategyパターンは、振る舞いを外部から与えることで、オブジェクトの振る舞いを柔軟に差し替えるパターンです。ここでは、「星のカービィ[1]」という作品に出てくるカービィを例に挙げて、Strategyパターンについて解説します。

Strategyパターンの実装例

カービィは、敵の能力をコピーすることができ、コピーした能力によって様々な攻撃が行えます。
このカービィの能力を、Strategyパターンを使って実装したいと思います。
下記に実装例を示します。

<?php

namespace Kirby;

interface Ability
{
    public function fight(): void;
}

class Normal implements Ability
{
    public function fight(): void
    {
        print '敵を吸い込む!';
    }
}

class Kirby
{
    private Ability $ability;

    public function __construct() {
        $this->ability = new Normal();
    }

    public function copy(Ability $ability): void
    {
        $this->ability = $ability;
    }

    public function uncopy(): void
    {
        $this->ability = new Normal();
    }

    public function fight(): void
    {
        $this->ability->fight();
    }
}
<?php

namespace Ability;

use Kirby\Ability;

class Ninja implements Ability
{
    public function fight(): void
    {
        print '手裏剣で攻撃!';
    }
}

class Cook implements Ability
{
    public function fight(): void
    {
        print '敵を料理して食べる!';
    }
}


図1: クラスとパッケージ間の依存関係
Kirbyクラスのcopyメソッドで、外部からの振る舞いを与えています。Abilityインターフェイスを実装したクラスであれば、なんでもcopyメソッドに与えることができます。そして、Kirbyクラスのfightメソッドで、外部から与えられた振る舞いを呼び出しています。
下記の使用例のように、fightメソッドの実行結果は、copyメソッドに与えられた振る舞いによってその都度変化します。

// 使用例
$kirby = new Kirby();
$kirby->fight(); // 敵を吸い込む!

$kirby->copy(new Ninja());
$kirby->fight(); // 手裏剣で攻撃!

$kirby->copy(new Cook());
$kirby->fight(); // 敵を料理して食べる!

この部分がStrategyパターンの特徴で、処理の実行中でも柔軟に振る舞いを差し替えることができます。

Strategyパターンの利点

Strategyパターンの利点を下記に挙げます。

  • 新しい振る舞いを簡単に追加でき、変更容易性が高まる
  • 関心の分離が行える

それぞれの利点について解説します。

新しい振る舞いを簡単に追加でき、変更容易性が高まる

このことについて解説するために、カービィの新しいコピー能力を先ほどの実装例に追加したいと思います。

<?php

namespace Ability;

use Kirby\Ability;

// ファイアのコピー能力を追加
class Fire implements Ability
{
    public function fight(): void
    {
        print '火を吹いて攻撃!';
    }
}
// 使用例
$kirby = new Kirby();
$kirby->copy(new Fire());
$kirby->fight(); // 火を吹いて攻撃


図2: 新しいコピー能力を追加した後の依存関係
このように、Kirbyパッケージを一切変更せずに、Fireクラスを追加するだけで新しいコピー能力を追加することができました。Fireクラスの追加は、他のパッケージへの変更の影響を一切気にせずに行えるため、不具合のリスクがとても少なく簡単に行えます。

既存の成果物を変更せず拡張できるようにすべき[2]とするオープン・クローズドの原則があります。Strategyパターンを使うことで、新しい振る舞いを追加したい場合に、既存の処理を一切修正することなく、新しい処理を追加するだけで済みます。そのため、この原則に合致した実装になり変更容易性が高まります。

関心の分離が行える

Strategyパターンを使うことで、「何をするのか」と「どうやるか」のそれぞれの責務を分離することができます。例えば、Kirbyクラスのfightメソッドでは、「戦う」という本質的な部分だけを知っていればよく、「どのように戦うか」の具体的な詳細部分はAbilityインターフェイスを実装したクラスに委譲しています。
これにより、変更頻度が高い具体的な詳細部分を変更頻度が低い本質部分から切り離すことができ、変更の影響範囲を小さく閉じることができます。また、それぞれのクラスが持つ責務が小さくなるため、可読性も向上します。

Strategyパターンを使わないとどうなるのか?

Strategyパターンを使わないとどのような問題が起こるのかについてまとめます。
かなり無理やり感がある実装になってしまいますが、カービィのコピー能力をStrategyパターンを使わずに、switch文を使って実装した例を下記に示します。

<?php

namespace Kirby;

class Kirby
{
    private string $abilityName;

    public function __construct() {
        $this->abilityName = 'Normal';
    }

    public function copy(string $abilityName): void
    {
        $this->abilityName = $abilityName;
    }

    public function uncopy(): void
    {
        $this->abilityName = 'Normal';
    }

    public function fight(): void
    {
        switch ($this->abilityName) {
            case 'Ninja':
                print '手裏剣で攻撃!';
                break;
            case 'Cook':
                print '敵を料理して食べる!';
                break;
            default:
                print '敵を吸い込む!';
                break;
        }
    }
}

Strategyパターンを使わないと、fightメソッドの処理が条件分岐でややこしくなってしまいました。
カービィのコピー能力がさらに増えたときに手に負えなくなりそうです。
ここでまた、カービィの新しいコピー能力を追加したいと思います。追加した例を下記に示します。

class Kirby
{
    // 省略

    public function fight(): void
    {
        switch ($this->abilityName) {
            case 'Ninja':
                print '手裏剣で攻撃!';
                break;
            case 'Cook':
                print '敵を料理して食べる!';
                break;
            // コピー能力を追加
            case 'Fire':
                print '火を吹いて攻撃!';
                break;
            default:
                print '敵を吸い込む!';
                break;
        }
    }
}

今回の場合、新しいコピー能力を追加する時にfightメソッド自体を変更する必要があります。これでは、既存のNinjaやCookの処理にバグを埋め込んでしまう可能性が少なからずあります。また、Kirbyクラスに依存している全てのパッケージに変更の影響が出てしまいます。Strategyパターンを使った場合と比べて明らかに変更の影響範囲が広く、それだけ不具合のリスクが高いです。

このように、Strategyパターンを使わないと、新しい振る舞いを追加する際に変更の影響範囲が広いため変更容易性が低下してしまいます。また、クラスが持つ責務も大きくなり、条件分岐も増えるため可読性が低下してしまいます。

まとめ

カービィのコピー能力を例に、振る舞いを動的に切り替えるためのStrategyパターンを解説しました。Strategyパターンを使うことで、新しい振る舞いを簡単に追加できるようになり、コードの可読性も向上します。
複雑な条件分岐で複数の振る舞いを無理やり実装するのではなく、Strategyパターンを使って今後の拡張性を意識した設計にすることは非常に重要です。

脚注
  1. https://www.kirby.jp/ ↩︎

  2. Robert C. Martin他, Clean Architecture, 2018年7月27日, 株式会社ドワンゴ, P87, ↩︎

TREASURE FACTORY TECH BLOG

Discussion