🍣

Commandパターンについて

に公開

はじめに

Commandパターンは、処理そのものをオブジェクトとしてカプセル化することにより、リクエストを発行する側とリクエストを実行する側のそれぞれを疎結合にできるパターンです。
ゲームコントローラのキー割り当てを例に挙げて、Commandパターンについて解説します。

実装例

このゲームコントローラにはAボタンがあり、このAボタンに「攻撃する」操作と「ジャンプする」操作を簡単に切り替えられるように実装します。

<?php

namespace Shared;

interface Command
{
    public function execute(): void;
}
<?php

namespace Player;

class Player
{
    public function jump(): void
    {
        print 'ジャンプする';
    }

    public function attack(): void
    {
        print '攻撃する';
    }
}

class JumpCommand implements Command
{
    public function __construct(private Player $player) {}

    public function execute(): void
    {
        $this->player->jump();
    }
}

class AttackCommand implements Command
{
    public function __construct(private Player $player) {}
    
    public function execute(): void
    {
        $this->player->attack();
    }
}
<?php

namespace Client;

use Player\AttackCommand;
use Player\JumpCommand;
use Shared\Command;

class AButton
{
    private Command $command;

    public function changeKeyMapping(Command $command): void
    {
        $this->command = $command;
    }

    public function press(): void
    {
        $this->command->execute();
    }
}
// 使用例
$player = new Player();

$aButton = new AButton();
$aButton->changeKeyMapping(new JumpCommand($player));
$aButton->press(); // ジャンプする

// キー割り当てを変更する
$aButton->changeKeyMapping(new AttackCommand($player));
$aButton->press(); // 攻撃する


図1: パッケージ間の依存関係
AButtonクラスのchangeKeyMappingメソッドで、Aボタンに様々な操作を割り当てています。
そして、pressメソッドでAボタンに割り当てられた操作を実行しています。

利点

Commandパターンの利点を下記に示します。

  • 疎結合化による高い拡張性と保守性
  • 操作のオブジェクト化による応用

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

疎結合化による高い拡張性と保守性

AButtonクラスは、自分が保持している$commandが具体的にどのような処理を実行するのかを全く知りません。とにかく内部で、$this->command->execute()を実行しているだけです。

一方、AttackCommandやJumpCommandクラスは、自分が具体的にどのような処理を実行するのかを知っています。しかし、これらの処理がAButtonによって引き起こされたことは知りません。もしかしたら、CButtonから呼ばれるかもしれないし、もはやゲームコントローラではなくキーボードから呼ばれる可能性だってあります。

したがって、Commandパターンを使うことにより、「ボタンを押す」というリクエストを発行する側と、「プレイヤーが攻撃やジャンプをする」というリクエストを実行する側を完全に分離することができます。

これにより、「防御する」などの新しい操作をPlayerパッケージに追加しても、その変更の影響がAButtonなどのClientパッケージ側に出てしまうことはありえません。

<?php

namespace Player;

class Player
{
    // 省略

    // 新しく防御するメソッドを追加
    public function defend(): void
    {
        print '防御する';
    }
}
<?php

namespace Player;

use Player\Player;
use Shared\Command;


// 新しく防御コマンドを追加
class DefendCommand implements Command
{
    public function __construct(private Player $player) {}
    
    public function execute(): void
    {
        $this->player->defend();
    }
}
// Clientパッケージ側は一切変更せずに、
// 新しく追加した防御するコマンドをボタンに割り当てられる
$aButton->changeKeyMapping(new DefendCommand($player));
$aButton->press(); // 防御する

同じように、CButtonなどの新しいボタンをClientパッケージに追加しても、その変更の影響が攻撃やジャンプといったPlayerパッケージ側に出てしまうことはありえません。

<?php

namespace Client;

use Player\Player;
use Player\AttackCommand;
use Player\JumpCommand;
use Shared\Command;

// 新しくCボタンを追加
class CButton
{
    private Command $command;

    public function changeKeyMapping(Command $command): void
    {
        $this->command = $command;
    }

    public function press(): void
    {
        $this->command->execute();
    }
}
// 新しく追加したCボタンに機能を割り当てて実行する
// Playerパッケージは一切変更せずに、Cボタンに機能を割り当てられた
$cButton = new CButton();
$cButton->changeKeyMapping(new JumpCommand($player));
$cButton->press(); // ジャンプする

このように、Commandパターンを使うことにより、リクエストを出す側(Clientパッケージ)とリクエストを実行する側(Playerパッケージ)の両者の独立性が高まり、それぞれを個別に修正・拡張できるようになります。

操作のオブジェクト化による応用

Commandパターンは、操作そのものがオブジェクトになっています。

// 操作したいことがオブジェクトになっている
class AttackCommand implements Command
{
    public function __construct(private Player $player) {}
    
    public function execute(): void
    {
        $this->player->attack();
    }
}

そのため、操作をすぐには実行せず、一旦その操作をQueueにためておき、バックグラウンドで非同期に実行することが可能です。
時間がかかってしまう重い処理をQueueにためておき、バックグラウンドで非同期に実行できるようにしておけば、ユーザーを待たせてアプリケーションが固まってしまうのを防ぎ、快適な操作性を維持できます。
他には、過去のリクエストを保存して巻き戻せるようにする(アンドゥ/リドゥ)活用例もあります。

まとめ

Commandパターンは、操作をオブジェクトとしてカプセル化し、リクエストの発行者と実行者を疎結合にするデザインパターンです。
この記事では、ゲームコントローラのキー割り当てを例に挙げてCommandパターンについて解説しました。Commandパターンを使うことにより下記の利点があります。

疎結合による高い拡張性と保守性

「防御する」などの新しい操作をPlayerパッケージに追加しても、Clientパッケージに変更の影響が出てしまうことはありません。同じように、新しいボタンをClientパッケージに追加しても、Playerパッケージに変更の影響が出てしまうことはありません。このように、お互いのパッケージに影響を与えることなく、独立して変更・追加ができます。

操作のオブジェクト化による応用

操作自体がオブジェクトになっているため、操作をQueueにためてバックグラウンドで非同期に実行することができます。また、実行履歴を保存して「アンドゥ/リドゥ機能」を実装することも可能です。

Discussion