広告

Java : Command パターン (図解/デザインパターン)

Command パターンとは、GoF によって定義されたデザインパターンの1つです。
処理の依頼(リクエスト) と 処理の実行を分離する、という設計です。

本記事では、Command パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

図解/デザインパターン一覧

概要

Command パターン(英: command pattern)は、オブジェクト指向プログラミングにおいて命令あるいは動作をオブジェクトで表現するデザインパターンの一種である。リクエストのために必要な手続きとデータをCommandオブジェクトとしてカプセル化した上で取り回し[1]、必要に応じて実行(execute)するパターンである。

Command パターンとは、

  • 「処理の依頼(リクエスト)」と「処理の実行」を分離する

という設計です。

これにより、

  • 処理の リクエスト はしたけど、処理の 実行 は遅らせる
  • リクエスト単位で、処理のアンドゥ(元に戻す) 機能を実現する
  • 処理の リクエスト は複数のサブスレッドからでも OK
    ただし、処理の 実行 は単一のスレッド上で実行させたい

といったことが可能になります。

Command は、日本語的に発音すると「コマンド」となります。
意味は「命令」ですね。


【Command パターンのイメージ】

イメージ図1

ある1つのオブジェクトがあります。
このオブジェクトを「ターゲット」オブジェクトとしましょう。

「ターゲット」の 処理を実行 するには メソッドを呼び出す ことになります。
ただし、メソッドを呼び出すと、すぐに処理は実行されることでしょう。

もしも …

  • すぐには処理を実行したくない
  • 都合のよいタイミングで実行したい

そんなケースが必要になったらどうでしょうか?

例えば、プリンターに対してテキストの「印刷」を要求します。
しかし、別のテキストを印刷中であれば、印刷が終わるまで「待つ」必要があります。


イメージ図2

Command パターンでは、処理の依頼(リクエスト) を「コマンド」オブジェクトとして表します。
そして、「コマンド」は 依頼受取人 によって保持されます。

「コマンド」オブジェクトは …

  • 「ターゲット」のどのメソッドを呼び出すべきか?
  • そのメソッドの呼び出しに必要なパラメータ

を保持しています。
つまり、いつでも好きなタイミングで「ターゲット」のメソッドを呼び出せるわけですね。

【補足】

Command パターンは、GUI のフレームワークなどに利用されています。
イベントドリブン をご存じのかたはイメージしやすいのかな、と思います。

また、Memento パターン と組み合わせて

  • 元に戻す(アンドゥ)
  • やり直し(リドゥ)

といった機能の実現にも利用できるでしょう。

関連記事:


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、Command パターンの一般的なクラス図とシーケンス図です。
それでは、このクラス図をそのままコードにしてみましょう。

まずは Receiver クラスです。
少し役割が分かりづらいかもしれないので 具体的な例 も合わせてご確認ください。

public class Receiver {
    public void action1(int param) {
        System.out.println("Action 1 : " + param);
    }

    public void action2(String param) {
        System.out.println("Action 2 : " + param);
    }
}

Command インタフェースは、execute メソッドを宣言するだけです。

public interface Command {
    void execute();
}

Command インタフェースの実装クラスは、

  • Receiver オブジェクト
  • メソッドの呼び出しに必要なパラメータ

をフィールドに持ちます。

public class ConcreteCommand1 implements Command {
    private final Receiver receiver;
    private final int param;

    public ConcreteCommand1(Receiver receiver, int param) {
        this.receiver = receiver;
        this.param = param;
    }

    @Override
    public void execute() {
        receiver.action1(param);
    }
}

public class ConcreteCommand2 implements Command {
    private final Receiver receiver;
    private final String param;

    public ConcreteCommand2(Receiver receiver, String param) {
        this.receiver = receiver;
        this.param = param;
    }

    @Override
    public void execute() {
        receiver.action2(param);
    }
}

最後に Invoker クラスです。
Command オブジェクトを保持・実行します。

フィールドには 両端キュー(Deque) を使っています。

public class Invoker {
    private final Deque<Command> commands = new ArrayDeque<>();

    public void add(Command command) {
        commands.addLast(command);
    }

    public void execute() {
        if (commands.isEmpty()) {
            return;
        }

        final var command = commands.removeFirst();
        command.execute();
    }
}

それでは、これらのクラスを使ってみましょう。

final var invoker = new Invoker();
final var receiver = new Receiver();

invoker.add(new ConcreteCommand1(receiver, 123));
invoker.add(new ConcreteCommand2(receiver, "ABC"));

invoker.execute();

// 結果
// ↓
//Action 1 : 123

invoker.execute();

// 結果
// ↓
//Action 2 : ABC

結果も問題なしですね。
Command オブジェクトを経由して、Reciver オブジェクトの各メソッドが呼び出せました。


具体的な例

もう少し具体的な例も見てみましょう。
テキストを編集する、とてもシンプルな TextEditor クラスを考えてみます。

クラス図2

TextEditor クラスには次のメソッドを用意します。

  • append : 末尾に文字列を追加
  • deleteEnd : 末尾から指定した長さの文字列を削除
  • getText : テキスト全体を取得

TextEditor クラスの使用例は次のようになります。

final var editor = new TextEditor();

editor.append("abc");
System.out.println(editor.getText()); // abc

editor.append("123456");
System.out.println(editor.getText()); // abc123456

editor.deleteEnd(3);
System.out.println(editor.getText()); // abc123

Command パターンを使ってアンドゥ機能を実装

さて、この TextEditor クラスですが、しばらくして

  • 「アンドゥ(元に戻す)」機能

が欲しくなりました。

今回は Command パターンを使って実現してみましょう。

クラス図3

Command パターン の Reciver クラスに相当するのが、今回の TextEditor クラスです。

public class TextEditor {
    private final StringBuilder text = new StringBuilder();

    public void append(String str) {
        text.append(str);
    }

    public String deleteEnd(int length) {
        final var start = text.length() - length;

        final var deletedStr = text.substring(start);
        text.delete(start, text.length());

        return deletedStr;
    }

    public String getText() {
        return text.toString();
    }
}

deleteEnd メソッドでは、実際に削除された文字列を戻り値として返します。
(アンドゥ機能のため)

次に Command インタフェースとその実装クラスです。
execute メソッドの戻り値として、アンドゥ用の Command オブジェクトを返します。

public interface Command {
    Command execute();
}

public class AppendCommand implements Command {
    private final TextEditor editor;
    private final String str;

    public AppendCommand(TextEditor editor, String str) {
        this.editor = editor;
        this.str = str;
    }

    @Override
    public Command execute() {
        editor.append(str);

        // 追加した文字列と同じ長さの削除を「アンドゥ」とします。
        return new DeleteEndCommand(editor, str.length());
    }
}

public class DeleteEndCommand implements Command {
    private final TextEditor editor;
    private final int length;

    public DeleteEndCommand(TextEditor editor, int length) {
        this.editor = editor;
        this.length = length;
    }

    @Override
    public Command execute() {
        final var deletedStr = editor.deleteEnd(length);

        // 削除した文字列の追加を「アンドゥ」とします。
        return new AppendCommand(editor, deletedStr);
    }
}

最後に Invoker クラスです。

今回は execute メソッドで Command オブジェクトをすぐに実行します。
そして、アンドゥ用の Command オブジェクトをフィールドに保持します。

public class Invoker {
    private final Deque<Command> undoCommands = new ArrayDeque<>();

    public void execute(Command command) {
        final var undoCommand = command.execute();

        // 多くなりすぎないように
        if (undoCommands.size() > 5) {
            undoCommands.removeFirst();
        }

        undoCommands.addLast(undoCommand);
    }

    public void undo() {
        if (undoCommands.isEmpty()) {
            return;
        }

        final var command = undoCommands.removeLast();
        command.execute();
    }
}

それでは、これらのクラスを使ってみましょう。

final var editor = new TextEditor();
final var invoker = new Invoker();

invoker.execute(new AppendCommand(editor, "abc"));
System.out.println(editor.getText()); // abc

invoker.execute(new AppendCommand(editor, "123456"));
System.out.println(editor.getText()); // abc123456

invoker.execute(new DeleteEndCommand(editor, 3));
System.out.println(editor.getText()); // abc123

invoker.undo();
System.out.println(editor.getText()); // abc123456

invoker.undo();
System.out.println(editor.getText()); // abc

結果も問題なしですね。
undo メソッドで、処理が「元に戻る」ことを確認できました。

まとめ

Command パターンとは、

  • 「処理の依頼(リクエスト)」と「処理の実行」を分離する

という設計です。

これにより、

  • 処理の リクエスト はしたけど、処理の 実行 は遅らせる
  • リクエスト単位で、処理のアンドゥ(元に戻す) 機能を実現する
  • 処理の リクエスト は複数のサブスレッドからでも OK
    ただし、処理の 実行 は単一のスレッド上で実行させたい

といったことが可能になります。

有効に活用していきたいですね。


関連記事

ページの先頭へ