
Commandパターンは、「操作をオブジェクトとして扱う」デザインパターンです。
処理(命令)を呼び出し側と実行側で分離できるのが特徴です。
例えば「操作の履歴」「アンドゥ(取り消し)」などを簡単に実現できる。
要するに、「実行したい処理」をオブジェクトとして切り出し、「いつ・どこで・どう実行するか」を柔軟にコントロールできる仕組みです。
構造イメージ
| 役割 |
説明 |
| Command(コマンド) |
実行される処理を表すオブジェクト |
| Receiver(受信者) |
実際に処理を行う対象(例:ライト、テレビなど) |
| Invoker(呼び出し元) |
コマンドを実行する役(ボタンなど) |
| Client(依頼者) |
コマンドをセットアップして使う人 |
サンプルコード
ライトのON/OFFのサンプルコードです。
// Receiver(命令を実際に実行する側)
class Light {
on() {
console.log('ライトをつけました');
}
off() {
console.log('ライトを消しました');
}
}
// Command(操作をオブジェクト化)
class LightOnCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.on();
}
undo() {
this.light.off();
}
}
class LightOffCommand {
constructor(light) {
this.light = light;
}
execute() {
this.light.off();
}
undo() {
this.light.on();
}
}
// Invoker(命令を実行するボタン的な存在)
class RemoteControl {
setCommand(command) {
this.command = command;
}
pressButton() {
this.command.execute();
}
pressUndo() {
this.command.undo();
}
}
// Client(使う側)
const light = new Light();
const lightOn = new LightOnCommand(light);
const lightOff = new LightOffCommand(light);
const remote = new RemoteControl();
remote.setCommand(lightOn);
remote.pressButton(); // ライトをつけました
remote.pressUndo(); // ライトを消しました
remote.setCommand(lightOff);
remote.pressButton(); // ライトを消しました
remote.pressUndo(); // ライトをつけました
メリット
・操作をオブジェクト化できる(履歴管理が容易)
・実行と依頼を分離できる(柔軟な構造)
・アンドゥ/リドゥ処理が自然に実装できる
・コマンドをキューやスケジューラに入れて非同期制御も可能
デメリット
・クラスやオブジェクトが増えすぎる
・設計がやや抽象的で理解しづらい
・状態管理やUndo処理の実装が煩雑になる
・小規模用途にはオーバーエンジニアリング
・デバッグがしにくい
応用例
・ゲームでの操作履歴(元に戻す機能)
・Webアプリでの「やり直し」機能
・バッチ処理キュー
・マクロ記録(複数操作をまとめて実行)
サンプルコード2
Undo,Redoを操作するサンプルコードを作ってみました。
// Receiver(実際に操作を受ける側)
class TextEditor {
constructor() {
this.text = "";
}
addText(newText) {
this.text += newText;
}
removeText(length) {
this.text = this.text.slice(0, -length);
}
getText() {
return this.text;
}
}
// Command(操作をオブジェクト化)
class AddTextCommand {
constructor(editor, text) {
this.editor = editor;
this.text = text;
}
execute() {
this.editor.addText(this.text);
}
undo() {
this.editor.removeText(this.text.length);
}
}
// Invoker(Commandを管理・実行する)
class CommandManager {
constructor() {
this.history = [];
this.redoStack = [];
}
executeCommand(command) {
command.execute();
this.history.push(command);
this.redoStack = []; // redoはリセット
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo() {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
// 実行例(Client)
const editor = new TextEditor();
const manager = new CommandManager();
manager.executeCommand(new AddTextCommand(editor, "Hello "));
manager.executeCommand(new AddTextCommand(editor, "World!"));
console.log(editor.getText()); // "Hello World!"
manager.undo();
console.log(editor.getText()); // "Hello "
manager.redo();
console.log(editor.getText()); // "Hello World!"
ポイント解説
Command が「何をしたか(テキスト+長さ)」を覚えているのがポイントです。
CommandManager が「履歴」と「Redo用スタック」を持つことで、undo() / redo() を繰り返せるようになっています。
これを応用することで、いろんなシステムでのUndo,Redoを機能追加できますね。
あとがき
個人的に思ったのは、オブジェクト指向のような感じで、クラスとメソッドをセットするようなイメージですね。
事前のセットがめんどくさいですが、一度セットしてしまうと、イベント操作でも迷う事なくスラスラかけるので、事前準備型デザインパターンということがよくわかりました。
ただ、依存がディープに発生するのが容易に想像できるので、余程設計をしっかりしておかないと、機能追加時に設計破綻しかねるところは注意ポイントですね。
0 件のコメント:
コメントを投稿