Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

PHPによるデザインパターン入門 - Command〜要求をクラスで表す

このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものgithubに上げてありますのでそちらもどうぞ。

GoF本における分類

 振る舞い+オブジェクト

はじめに

 ここではCommandパターンについて説明します。
 commandという単語は「命令」という意味ですね。「コマンド」と書くと、DOSプロンプトやUNIXLinuxのコンソールで入力するコマンドを連想される方も多いかと思います。
 Commandパターンはその名の通り「命令」そのものをクラスとして表すパターンです。しかし、「命令」をクラスにするとどの様なことになるのでしょうか?

たとえば

 ほとんどのアプリケーションでは、利用者はアプリケーションに何らかの要求を出し、アプリケーションは要求を受け取って処理を実行しています。先に出てきたDOSLinuxなどのコマンドもその1つですし、WindowsX WindowといったGUIでの操作も含まれます。
 たとえば、ファイルを圧縮する場合を考えてみましょう。Windowsの場合、圧縮ソフトにファイルをドラッグアンドドロップするといった操作になりますし、Linuxなどのコンソールから実行する場合、圧縮コマンドにファイル名を指定して実行するでしょう。

 この時、利用者はソフトウェアやコマンドを通じて「圧縮する」という要求を出し、対象のファイルがその要求を受け取っている、と考えることができます。また、ファイルに別の操作をおこなう、つまり別な要求を出す場合は、「処理をおこなうソフトやコマンド」を差し替えたものと考えられるでしょう。
 オブジェクト指向プログラミングにおいて、「要求を送る」ということはオブジェクトのメソッドを呼び出すということになります。しかし、要求が複雑になったり要求の種類が多くなると、その実装にも限界が出てきますし保守性も悪くなってしまいます。
 そこで、「要求を送る」「要求を受け取る」という考えに基づいて、「要求」自身をクラスとしてまとめてしまうとどうでしょうか?
 そうすると、具体的な要求を1つのオブジェクトとして扱うことができ、複雑な要求の場合でも容易に扱えるようになります。また、送る要求オブジェクトを変えることで、異なる要求を実現こともできそうです。
 Commandパターンは、このような特徴を持っているパターンです。

Commandパターンとは?

 Commandパターンはオブジェクトの振る舞いに注目したパターンで、「要求」そのものをクラスとして表し、「要求を送る側」と「要求を受け取る側」を分離することを目的としています。
 GoF本では、Commandパターンは以下のように定義されています。

要求をオブジェクトとしてカプセル化することによって、様々な要求または要求からなるキューやログによりクライアントをパラメータ化する。そして、取り消し可能な操作をサポートする。

 Commandパターンでは、異なる種類の要求に対する処理を同じAPIを持つクラスとして実装します。その結果、処理クラスのインスタンスを切り替えるだけで、様々な要求に対する処理を実行できるようなります。また、Commandパターンを適用すると、新しい要求に対する処理クラスを実装するだけで、既存のクラスを修正することなく対応可能になります。

Commandパターンの構造

 Commandパターンのクラス図と構成要素は、次のようになります。


  • Commandクラス

 命令を実行するためのAPIを定義します

  • ConcreteCommandクラス

 Commandクラスのサブクラスで、Commandクラスで定義されたAPIを実装します。

  • Invokerクラス

 命令実行の要求を出すクラスです。

  • Receiverクラス

 命令をどの様に実行するかを知っている唯一のクラスです。任意のクラスがReceiverクラスになることができます。

Commandパターンのメリット

 Commandパターンのメリットとしては、次のものが挙げられます。

  • 既存のコードを修正することなく機能拡張できる

 Commandパターンを適用すると、要求の受付と要求に対応する処理を切り離して実装できます。その結果、新しい要求が追加された場合でも既存のクラスを修正する必要がなく、追加された要求を処理するためのクラスを実装するだけで済みます。

  • クラスの再利用性を向上させる

 命令そのものが独立したクラスとして実装されますので、他のアプリケーションでの再利用がしやすくなります。

  • 処理のキューイング

 要求と実際の実行を別のタイミングで実施することができるようになります。

  • UndoやRedoのサポート

 Commandクラスに実行したコマンド結果を保持しておくことで、Undo機能やRedo機能を実現することができます。

Commandパターンの適用例

 早速、Commandパターンを適用してみましょう。
 ここでは、ファイルの作成・圧縮・コピーをおこなうアプリケーションを用意しました。Commandパターンを適用し、ファイルに対する操作をコマンドとして定義しています。
 まずは、Fileクラスから見ていきます。FileクラスはReceiverクラスに相当するクラスです。作成・圧縮・コピーそれぞれの処理に対するメソッドがありますが、今回はメッセージを表示するだけの実装としています。

Fileクラス(File.class.php
<?php
/**
 * Receiverクラスに相当する
 */
class File
{
    private $name;
    public function __construct($name)
    {
        $this->name = $name;
    }
    public function getName()
    {
        return $this->name;
    }
    public function decompress()
    {
        echo $this->name . 'を展開しました<br>';
    }
    public function compress()
    {
        echo $this->name . 'を圧縮しました<br>';
    }
    public function create()
    {
        echo $this->name . 'を作成しました<br>';
    }
}

 続いて、要求を処理する側のクラス群を見ていきましょう。
 Commandインターフェースは、すべてのコマンドに共通のAPIであるexecuteメソッドを宣言しています。

Commandインターフェース(Command.class.php
<?php
/**
 * Commandクラスに相当する
 */
interface Command {
    public function execute();
}

 このCommandインターフェースを実装したクラスが、TouchCommandクラス、CompressCommandクラス、CopyCommandクラスです。ConcreteCommandクラスに相当し、Commandインターフェースで宣言されたexecuteメソッドを実装しています。
 これらのクラスは、コンストラクタでFileオブジェクトを受け取り、executeメソッドでそれぞれの要求に対する処理をおこないますが、具体的な処理は受け取ったFileオブジェクトに任せています。

TouchCommandクラス(TouchCommand.class.php
<?php
require_once 'Command.class.php';
require_once 'File.class.php';

/**
 * ConcreteCommandクラスに相当する
 */
class TouchCommand implements Command
{
    private $file;
    public function __construct(File $file)
    {
        $this->file = $file;
    }
    public function execute()
    {
        $this->file->create();
    }
}
CompressCommandクラス(CompressCommand.class.php
<?php
require_once 'Command.class.php';
require_once 'File.class.php';

/**
 * ConcreteCommandクラスに相当する
 */
class CompressCommand implements Command
{
    private $file;
    public function __construct(File $file)
    {
        $this->file = $file;
    }
    public function execute()
    {
        $this->file->compress();
    }
}
CopyCommandクラス(CopyCommand.class.php
<?php
require_once 'Command.class.php';
require_once 'File.class.php';

/**
 * ConcreteCommandクラスに相当する
 */
class CopyCommand implements Command
{
    private $file;
    public function __construct(File $file)
    {
        $this->file = $file;
    }
    public function execute()
    {
        $file = new File('copy_of_' . $this->file->getName());
        $file->create();
    }
}

 QueueクラスはCommandオブジェクトを保持するInvokerクラスに相当するクラスで、実際にCommandオブジェクトを実行します。runメソッドを呼び出すと、内部に保持したCommandオブジェクトを順に実行します。

Queueクラス(Queue.class.php
<?php
require_once 'Command.class.php';

/**
 * Invokerクラスに相当する
 */
class Queue
{
    private $commands;
    private $current_index;
    public function __construct()
    {
        $this->commands = array();
        $this->current_index = 0;
    }
    public function addCommand(Command $command)
    {
        $this->commands[] = $command;
    }

    public function run()
    {
        while (!is_null($command = $this->next())) {
            $command->execute();
        }
    }

    private function next()
    {
        if (count($this->commands) === 0 ||
            count($this->commands) <= $this->current_index) {
            return null;
        } else {
            return $this->commands[$this->current_index++];
        }
    }
}

 次は、これまで説明してきたクラス群を利用するクライアント側のコードです。
 まず、Queueオブジェクトを生成した後、addCommandメソッドを使ってファイルに対する要求を表すCommandオブジェクトを追加しています。そして、最後に追加したCommandオブジェクトを実行しています。

クライアント側コード(command_client.php
<?php
require_once 'Queue.class.php';
require_once 'TouchCommand.class.php';
require_once 'CompressCommand.class.php';
require_once 'CopyCommand.class.php';
require_once 'File.class.php';

$queue = new Queue();
$file = new File("sample.txt");
$queue->addCommand(new TouchCommand($file));
$queue->addCommand(new CompressCommand($file));
$queue->addCommand(new CopyCommand($file));

$queue->run();

 ここで、それぞれの要求が実行される様子をシーケンス図を使って確認しておきましょう。
 Invokerクラスに相当するQueueオブジェクトが各ConcreteCommandオブジェクトを実行し、要求の受け取り側のFileオブジェクトにアクセスしている様子が分かりますね。

 最後に、このサンプルアプリケーションのクラス図となります。


適用例の実行結果

 Commandパターンを適用したサンプルの実行結果は、次のようになります。


Commandパターンのオブジェクト指向的要素

 Commandパターンは、「ポリモーフィズム」を利用したパターンになります。
 具体的な「要求」は、CommandクラスのサブクラスであるConcreteCommandクラスで表されます。また、ConcreteCommandクラスではCommandクラスで定義されたAPIを具体的に実装しています。
 また、Invokerクラスは内部にCommand型のオブジェクトを保持し、それらを実行します。この時、これらのオブジェクトはあくまでCommand型として扱われています。つまり、具体的なConcreteCommandクラスに依存していないのです。
 多くのデザインパターンでもポリモーフィズムを使って具体的なクラスを差し替え可能になっています(「交換可能性」といいます)。Commandパターンでは、変化する可能性がある「要求」にポリモーフィズムを使うことで、異なる要求を容易に差し替えられるようにしています。

関連するパターン

  • Compositeパターン

 複数のコマンドをまとめたマクロコマンドを実現するために利用されます。

  • Mementoパターン

 コマンドの実行履歴を管理するために、Mementoパターンが利用されることがあります。

  • Prototypeパターン

 Commandクラスを複製したい場合にPrototypeパターンが利用されることがあります。

まとめ

 ここでは「要求」そのものをクラスとして表し、「要求を送る側」と「要求を受け取る側」を分離するCommandパターンについて見てきました。