概要
操作そのものをオブジェクトとして表現し、実行・取り消し・やり直しを統一的に扱う。Command パターンは、テキストエディタの Undo/Redo やマクロ記録、トランザクションの補償処理など、操作の履歴管理が必要な場面で広く使われます。操作をオブジェクト化することで、実行と取り消しのロジックを1つのクラスに閉じ込められ、履歴のスタック管理も自然に書けます。この記事では、テキストエディタの挿入・削除操作を Command として実装し、Deque による Undo/Redo スタックの管理方法を整理します。Java 17 の record で操作結果を簡潔に返す方法、Java 21 の sealed interface でコマンドの種類を型安全に限定する方法も確認します。
使いどころ
テキストエディタの挿入・削除操作を Command オブジェクトとして記録し、Ctrl+Z / Ctrl+Y で Undo/Redo する
業務システムの一括更新処理をコマンド列として構築し、途中でエラーが起きたら逆順に補償トランザクションを実行する
ユーザーの操作ログをコマンドオブジェクトとして保存し、操作の再生やマクロ実行に利用する
コード例
import java.util.ArrayDeque;
import java.util.Deque;
public class CommandPatternDemo {
static class TextEditor {
private StringBuilder text = new StringBuilder();
public void insertText(int pos, String str) {
text.insert(pos, str);
}
public void deleteText(int pos, int len) {
text.delete(pos, pos + len);
}
public String getText() { return text.toString(); }
}
interface Command {
void execute();
void undo();
}
static class InsertCommand implements Command {
private final TextEditor editor;
private final int pos;
private final String str;
public InsertCommand(TextEditor editor, int pos, String str) {
this.editor = editor; this.pos = pos; this.str = str;
}
@Override public void execute() { editor.insertText(pos, str); }
@Override public void undo() { editor.deleteText(pos, str.length()); }
}
static class DeleteCommand implements Command {
private final TextEditor editor;
private final int pos;
private final int len;
private String deletedText;
public DeleteCommand(TextEditor editor, int pos, int len) {
this.editor = editor; this.pos = pos; this.len = len;
}
@Override public void execute() {
deletedText = editor.getText().substring(pos, pos + len);
editor.deleteText(pos, len);
}
@Override public void undo() { editor.insertText(pos, deletedText); }
}
record CommandResult(boolean success, String text, int historySize) {}
static class CommandHistory {
private final Deque<Command> history = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
public CommandResult execute(Command cmd, TextEditor ed) {
cmd.execute();
history.push(cmd);
redoStack.clear();
return new CommandResult(true, ed.getText(), history.size());
}
public CommandResult undo(TextEditor ed) {
if (history.isEmpty()) return new CommandResult(false, ed.getText(), 0);
var cmd = history.pop();
cmd.undo();
redoStack.push(cmd);
return new CommandResult(true, ed.getText(), history.size());
}
public CommandResult redo(TextEditor ed) {
if (redoStack.isEmpty()) return new CommandResult(false, ed.getText(), history.size());
var cmd = redoStack.pop();
cmd.execute();
history.push(cmd);
return new CommandResult(true, ed.getText(), history.size());
}
}
public static void main(String[] args) {
var editor = new TextEditor();
var history = new CommandHistory();
var r1 = history.execute(new InsertCommand(editor, 0, "Hello"), editor);
System.out.println("挿入後: " + r1.text());
var r2 = history.execute(new InsertCommand(editor, 5, ", Java"), editor);
System.out.println("挿入後: " + r2.text());
var u1 = history.undo(editor);
System.out.println("Undo後: " + u1.text());
var rd = history.redo(editor);
System.out.println("Redo後: " + rd.text());
}
}Version Coverage
record で CommandResult を定義でき、コマンド実行結果を簡潔に表現できる。var による型推論で履歴操作も読みやすい。
// Java 17: record で実行結果をまとめて返す
record CommandResult(boolean success,
String currentText, int historySize) {}
public CommandResult execute(Command cmd, TextEditor ed) {
cmd.execute();
history.push(cmd);
redoStack.clear();
return new CommandResult(true, ed.getText(),
history.size());
}Library Comparison
注意点
DeleteCommand の undo で元のテキストを復元するためには、execute 時に削除した文字列を保持しておく必要がある。保存し忘れると undo で復元できない
Redo スタックは新しいコマンドを実行したらクリアするのが一般的。クリアしないと Undo → 新操作 → Redo で不整合が起きる
コマンドオブジェクトが Receiver(エディタ等)への参照を保持するため、履歴が長くなるとメモリを圧迫する。履歴件数の上限を設けることを検討する
マルチスレッド環境でコマンド履歴を共有する場合は同期が必要。ConcurrentLinkedDeque や synchronized ブロックでの保護を検討する
FAQ
Command は操作のオブジェクト化で、実行と取り消しをセットにします。Strategy はアルゴリズムの差し替えが目的で、取り消しの概念は含みません。
Deque のサイズが上限を超えたら removeLast() で古い履歴を破棄します。LinkedList ベースの Deque なら O(1) で末尾を除去できます。
コマンドのリストを保持する MacroCommand を作り、execute で全コマンドを順に実行します。GoF の Composite と組み合わせた形です。