DP-14: Command パターン
操作をオブジェクトとして表現し、Undo/Redo やキューイング(順番待ち処理)を実現するパターンです。 テキストエディタの「元に戻す(Ctrl+Z)」機能を例に、Java 8 / 17 / 21 で解説します。
Command パターンとは
通常、メソッドは「呼び出されたその場で実行される」ものです。しかし Command パターンでは、 「何を実行するか」をオブジェクトとして表現します。 操作をオブジェクト化することで、実行をあとに回したり(キューイング)、 実行した操作を記録して取り消したり(Undo)、取り消した操作をもう一度やり直したり(Redo)することが可能になります。
Command パターンの構成要素
- Command インターフェース:
execute()とundo()を定義する - ConcreteCommand(具体的なコマンド): 実際の操作を実装する(InsertCommand、DeleteCommand など)
- Receiver(受信者): 実際の処理を行うオブジェクト(TextEditor など)
- Invoker(呼び出し側): コマンドを受け取って実行・履歴管理する(CommandHistory など)
Java 標準ライブラリでは javax.swing.Action インターフェースが Command パターンの代表例です。 メニュー項目とツールバーボタンが同じ Action オブジェクトを共有できるため、 「コピー」「ペースト」などの操作を一か所で定義して複数のUI部品から呼び出せます。
Command パターンが役立つ場面
- テキストエディタの Undo/Redo 機能
- データベーストランザクション(コミット・ロールバック)
- タスクキュー(操作を溜めておいて後でまとめて実行する)
- マクロ記録(複数の操作をまとめて再実行する)
サンプルコード
Java 8 では Command インターフェースに execute() と undo() を定義し、各コマンドクラスがそれぞれ実装します。DeleteCommand は execute() 時に削除したテキストを保存しておくことで undo() 時に復元できます。
よくあるミス・注意点
⚠️ undo() の実装を忘れる(execute() だけ実装してしまう)
Command インターフェースに undo() を定義しても、 各コマンドクラスに正しく実装しなければ Undo 機能は動作しません。 特に DeleteCommand は execute() 時に「削除した文字列」を保存しておかないと undo() 時に復元できないため、 execute() と undo() の両方を実装する際は「undo に必要な情報を execute 時に保存する」という設計が重要です。
⚠️ 新しい操作をしたときに redo 履歴をクリアしない
Undo した後に新しい操作を行ったときは、それ以前の redo 履歴をクリアする必要があります。 これをしないと「Undo → 新しい操作 → Redo」で意図しない操作が復元されてしまいます。 サンプルコードでは execute() の中でredoStack.clear() を呼び出してこれを防いでいます。
⚠️ 空の履歴で undo() を呼び出して例外が発生する
履歴が空の状態で undo() を呼び出すと、 スタックが空なので pop() で例外が発生する可能性があります。 必ず isEmpty() で確認してから処理するようにしましょう。
テストする観点
- execute() を呼び出した後、テキストが期待通りに変化していること
- execute() → undo() の順に呼び出すと、元の状態に戻ること(境界値: 1回の操作、複数回の操作)
- undo() → redo() の順に呼び出すと、undo 前の状態に戻ること
- 空の履歴で undo() を呼び出しても例外が発生しないこと(境界値: 履歴が空)
- 空の redo スタックで redo() を呼び出しても例外が発生しないこと
- Undo した後に新しい操作を行うと redo 履歴がクリアされること
- InsertCommand と DeleteCommand を組み合わせたとき、undo/redo が正しく連動すること