Java : Command パターン (図解/デザインパターン)
Command パターンとは、GoF によって定義されたデザインパターンの1つです。
処理の依頼(リクエスト) と 処理の実行を分離する、という設計です。
本記事では、Command パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Command パターンとは、
- 「処理の依頼(リクエスト)」と「処理の実行」を分離する
という設計です。
これにより、
- 処理の リクエスト はしたけど、処理の 実行 は遅らせる
- リクエスト単位で、処理のアンドゥ(元に戻す) 機能を実現する
- 処理の リクエスト は複数のサブスレッドからでも OK
ただし、処理の 実行 は単一のスレッド上で実行させたい
といったことが可能になります。
Command は、日本語的に発音すると「コマンド」となります。
意味は「命令」ですね。
【Command パターンのイメージ】
ある1つのオブジェクトがあります。
このオブジェクトを「ターゲット」オブジェクトとしましょう。
「ターゲット」の 処理を実行 するには メソッドを呼び出す ことになります。
ただし、メソッドを呼び出すと、すぐに処理は実行されることでしょう。
もしも …
- すぐには処理を実行したくない
- 都合のよいタイミングで実行したい
そんなケースが必要になったらどうでしょうか?
例えば、プリンターに対してテキストの「印刷」を要求します。
しかし、別のテキストを印刷中であれば、印刷が終わるまで「待つ」必要があります。
Command パターンでは、処理の依頼(リクエスト) を「コマンド」オブジェクトとして表します。
そして、「コマンド」は 依頼受取人 によって保持されます。
「コマンド」オブジェクトは …
- 「ターゲット」のどのメソッドを呼び出すべきか?
- そのメソッドの呼び出しに必要なパラメータ
を保持しています。
つまり、いつでも好きなタイミングで「ターゲット」のメソッドを呼び出せるわけですね。
【補足】
Command パターンは、GUI のフレームワークなどに利用されています。
イベントドリブン をご存じのかたはイメージしやすいのかな、と思います。
また、Memento パターン と組み合わせて
- 元に戻す(アンドゥ)
- やり直し(リドゥ)
といった機能の実現にも利用できるでしょう。
関連記事:
クラス図とシーケンス図
上の図は、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 クラスを考えてみます。
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 パターンを使って実現してみましょう。
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
ただし、処理の 実行 は単一のスレッド上で実行させたい
といったことが可能になります。
有効に活用していきたいですね。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン