はじめに
本記事は、GoFデザインパターン解説シリーズの1つです。
今回は、振る舞いに関するパターンの中のCommand(コマンド)パターンを解説します。
「操作(処理の依頼)そのものをオブジェクトとして扱う」という発想が特徴的なパターンです。
解決したい課題
「ある操作を実行する」というだけであれば、単純にメソッドを直接呼び出せば済みます。
しかし、以下のような要件が加わると、話が複雑になります。
- 実行した操作を後から取り消したい(Undo)
- 操作をキューに貯めておいて、後でまとめて実行したい
- 操作の実行履歴を記録したい
- 「ボタンを押したら何が起こるか」を、ボタン側とロジック側で疎結合にしたい(GUIのボタンが、具体的な処理内容を知らなくてもよいようにしたい)
こうした要件を、操作の呼び出し元に直接メソッド呼び出しを書くだけで実現しようとすると、呼び出し元のコードが「取り消し処理」「履歴管理」といった関心事まで抱え込んでしまい、複雑化してしまいます。
Commandパターンは、「操作を実行する」という処理そのものを、1つのオブジェクトとしてカプセル化することで、この問題を解決します。
操作をオブジェクトとして扱えるようになることで、キューに貯めたり、履歴として保存したり、Undo用の情報を持たせたりすることが容易になります。
クラス構成
- Command(コマンド): 操作を実行するメソッド(
execute())を宣言する共通インターフェース - ConcreteCommand(具体的なコマンド): 実際にどの処理を、どのオブジェクト(Receiver)に対して行うかを持つ、具体的なコマンドクラス
- Receiver(受信者): 実際の処理ロジックを持つオブジェクト
- Invoker(起動者): Commandを実行するきっかけとなるクラス(ボタン、メニュー項目など)。具体的な処理内容は知らない
Javaでの実装例
テキストエディタの「入力操作のUndo/Redo」を例に実装してみます。
Command:共通インターフェース
public interface Command {
void execute();
void undo();
}
Receiver:実際の処理ロジックを持つクラス
public class TextDocument {
private final StringBuilder content = new StringBuilder();
public void append(String text) {
content.append(text);
}
public void delete(int length) {
content.delete(content.length() - length, content.length());
}
public String getContent() {
return content.toString();
}
}
ConcreteCommand:具体的な操作をカプセル化するクラス
public class AppendTextCommand implements Command {
private final TextDocument document;
private final String textToAppend;
public AppendTextCommand(TextDocument document, String textToAppend) {
this.document = document;
this.textToAppend = textToAppend;
}
@Override
public void execute() {
document.append(textToAppend);
}
@Override
public void undo() {
// Undo(取り消し)に必要な情報(追加した文字数)を、コマンド自身が知っている
document.delete(textToAppend.length());
}
}
Invoker:コマンドを実行し、履歴を管理するクラス
import java.util.ArrayDeque;
import java.util.Deque;
public class CommandHistory {
private final Deque<Command> history = new ArrayDeque<>();
public void executeCommand(Command command) {
command.execute();
history.push(command); // 実行したコマンドを履歴として積んでおく
}
public void undoLast() {
if (!history.isEmpty()) {
Command lastCommand = history.pop();
lastCommand.undo();
}
}
}
利用側のコード
public class Main {
public static void main(String[] args) {
TextDocument document = new TextDocument();
CommandHistory history = new CommandHistory();
history.executeCommand(new AppendTextCommand(document, "Hello"));
history.executeCommand(new AppendTextCommand(document, ", World"));
history.executeCommand(new AppendTextCommand(document, "!"));
System.out.println("現在の内容: " + document.getContent());
history.undoLast(); // 最後の操作(!の追加)を取り消す
System.out.println("Undo後の内容: " + document.getContent());
history.undoLast(); // さらにもう1つ取り消す(, Worldの追加を取り消す)
System.out.println("Undo後の内容: " + document.getContent());
}
}
実行結果:
現在の内容: Hello, World!
Undo後の内容: Hello, World
Undo後の内容: Hello
CommandHistory(Invoker)は、「何らかのコマンドを実行する」「直近のコマンドを取り消す」ということだけを知っており、実際に「テキストを追加する」という処理内容そのものはAppendTextCommand(ConcreteCommand)の中にカプセル化されています。
この構造により、CommandHistoryのコードを一切変更せずに、新しい種類の操作(例えば「テキストの置換」)を、新しいCommand実装クラスとして追加するだけで対応できます。
使用場面
- 操作の取り消し(Undo)・やり直し(Redo)機能を実装したい場合
- 操作をキューに貯めて、後で実行したりバッチ処理したりしたい場合(ジョブキュー、タスクスケジューラ)
- GUIのボタンやメニュー項目と、実際の処理ロジックを疎結合にしたい場合
- 操作の実行履歴をログとして記録し、後から再生(リプレイ)したい場合
- マクロ機能(複数の操作をまとめて1つの操作として実行する)を実装したい場合
まとめ
| 項目 | 内容 |
|---|---|
| 解決する課題 | 操作の取り消し・キューイング・履歴管理などを、呼び出し元のコードを複雑化させずに実現したい |
| 実現方法 | 「操作の実行」自体を1つのオブジェクト(Command)としてカプセル化する |
| メリット | 操作をオブジェクトとして、貯める・記録する・取り消す、といった柔軟な扱いが可能になる |
| 典型例 | Undo/Redo機能、ジョブキュー、GUIのボタンとロジックの分離 |
おすすめ書籍
GoFの23パターンをより深く学びたい方には、以下の書籍がおすすめです。
Java言語のサンプルコードとUMLを使い、初心者にもわかりやすく解説されています。
Java言語で学ぶデザインパターン入門 第3版 [ 結城 浩 ]
※本リンクはアフィリエイトリンクを含みます。


