cv.cm

コーディング · デザインパターン

Command パターンとは?例でわかりやすく解説

約8分で読めます

結論

Command は振る舞いに関するデザインパターンで、リクエストを、実行に必要なすべてを含む独立したオブジェクトにします。リクエストがオブジェクトなので、保存したり、受け渡したり、キューに入れたり、ログに残したり、後で元に戻したりできます。コマンドを起動するオブジェクト(たとえばボタン)は、どのオブジェクトがどのように処理するかを知る必要がありません。

目的と解決する問題

目的:リクエストをオブジェクトとしてカプセル化することで、クライアントをさまざまなリクエストでパラメータ化し、リクエストのキューイングやログ記録、取り消し可能な操作をサポートすることです。Command は Gang of Four(GoF)の書籍にある振る舞いパターンの 1 つです。

テキストエディタを考えてみましょう。「太字にする」という同じ操作を、ツールバーのボタン、メニュー項目、キーボードショートカットの Ctrl+B から起動できます。これらがそれぞれエディタを直接呼び出すと、ロジックが 3 か所に重複します。さらに、エディタは Undo もサポートしなければなりません。ユーザーが Ctrl+Z を押したとき、プログラムは直前の操作が何で、どうすれば元に戻せるかを知っている必要があります。

Command は両方の問題を解決します。それぞれの操作が execute メソッド(必要なら undo メソッドも)を持つオブジェクトになります。ボタン、メニュー、ショートカットはすべて同じコマンドオブジェクトを持ちます。エディタは実行済みコマンドの履歴リストを保持するので、Undo は履歴の最後のコマンドを取り出して、その undo メソッドを呼ぶだけです。

構造と登場する役割

役割責務
Commandexecute メソッド(多くの場合は undo も)を持つインターフェース
Concrete commandReceiver とパラメータを保持し、execute の中で Receiver のメソッドを呼ぶ
Receiver実際の処理のやり方を知っているオブジェクト。たとえばドキュメントや銀行口座
Invokerコマンドに実行を依頼する。Undo や Redo のために履歴を保持することもある
Client(クライアント)Concrete command を生成し、Receiver と Invoker に結び付ける

TypeScript での実装:Undo と Redo

TextDocument が Receiver です。InsertText と DeleteLast は、自分自身を元に戻すのに十分な情報を記憶しているコマンドです。Editor は Invoker で、Undo 用と Redo 用の 2 つのスタックを持ちます。

interface Command {
  execute(): void;
  undo(): void;
}
class TextDocument {
  text = "";
}
class InsertText implements Command {
  constructor(private doc: TextDocument, private value: string) {}
  execute() { this.doc.text += this.value; }
  undo() { this.doc.text = this.doc.text.slice(0, -this.value.length); }
}
class DeleteLast implements Command {
  private removed = "";
  constructor(private doc: TextDocument, private count: number) {}
  execute() {
    this.removed = this.doc.text.slice(-this.count);
    this.doc.text = this.doc.text.slice(0, -this.count);
  }
  undo() { this.doc.text += this.removed; }
}
class Editor {
  private done: Command[] = [];
  private undone: Command[] = [];
  run(cmd: Command) { cmd.execute(); this.done.push(cmd); this.undone = []; }
  undo() { const c = this.done.pop(); if (c) { c.undo(); this.undone.push(c); } }
  redo() { const c = this.undone.pop(); if (c) { c.execute(); this.done.push(c); } }
}
const doc = new TextDocument();
const editor = new Editor();
editor.run(new InsertText(doc, "Hello"));
editor.run(new InsertText(doc, ", world"));
editor.run(new DeleteLast(doc, 7));
console.log(doc.text); // Hello
editor.undo();
console.log(doc.text); // Hello, world
editor.redo();
console.log(doc.text); // Hello

導入前と導入後

このパターンを使わない場合、UI はドキュメントを直接呼び出すため、何が起きたかを記録するものがなく、Undo は不可能です。

// Before: each trigger calls the receiver directly
boldButton.onclick = () => doc.makeBold(selection);
menuBold.onclick = () => doc.makeBold(selection);
// After: every trigger shares one command, and the editor records it
const bold = () => new MakeBold(doc, selection);
boldButton.onclick = () => editor.run(bold());
menuBold.onclick = () => editor.run(bold());

後者のコードは、前の例のような MakeBold コマンドクラスがあることを前提にした断片です。UI は太字にする方法を知らなくてよくなり、すべての操作が 1 か所を通るので、そこで記録・ログ出力・取り消しができます。

使うべき場面と避けるべき場面

  • Undo と Redo、マクロ(まとめて実行するコマンドのリスト)、コマンド履歴に使います。
  • 処理のキューイングやスケジューリングに使います。ジョブキューはコマンドオブジェクトを保存し、ワーカーが後でそれを実行します。
  • 操作のログ記録や再生に使います。たとえば、シリアライズしたコマンドを別のサーバーに送る場合です。
  • UI 要素と、それが起動するコードを切り離すために使います。
  • コールバックだけで足りる場合は使いません。TypeScript では関数がすでに第一級の値なので、button.onclick = save は Undo なしのコマンドとして十分に機能します。undo、表示用の名前、シリアライズのような追加の振る舞いが必要になったときに、コマンドクラスを導入しましょう。

実際の利用例

  • Java の Runnable インターフェースは、run というメソッドを 1 つ持つコマンドです。スレッドやエグゼキュータが、処理を後で実行するために使います。
  • Redux のアクションは、変更を記述するただのオブジェクトで、リデューサがそれを適用します。アクションのログによってタイムトラベルデバッグが可能になります。これは Command に近い仕組みで、ロジックがコマンドではなくリデューサにある点が異なります。
  • VS Code などの多くのエディタは、キー、メニュー、コマンドパレットに割り当てられる名前付きコマンドを登録します。
  • データベースのマイグレーションには、up と down の手順があることが多く、これはコマンドの execute と undo のペアにあたります。

関連するパターン

パターンCommand パターンとの違い
Strategy パターンStrategy は 1 つのタスクのやり方を差し替えます。Command は、保存・キュー化・取り消しができるように、何をするか(リクエスト)をパッケージ化します。
Memento パターンMemento は Undo のために状態のスナップショットを保存します。Command は操作を保存して、それを逆に実行します。元に戻すのが難しいときは、両者を組み合わせることがよくあります。
Chain of Responsibility パターンコマンドをハンドラの連鎖に沿って渡し、それを実行するハンドラを見つけられます。
Composite パターンマクロコマンドは Composite です。ほかのコマンドから成るコマンドになります。
Observer パターンObserver は多数のリスナーにイベントを通知します。Command は 1 つの Receiver に送られる 1 つのリクエストです。

よくある質問

Command パターンはどのように Undo を実現しますか?

各コマンドは、自分の execute を元に戻す undo メソッドを持ち、実行中に保存したデータを使うことがよくあります。Invoker は実行済みコマンドのスタックを保持し、最新のものの undo を呼びます。

Command と Strategy の違いは何ですか?

Strategy は同じタスクを行う異なる方法を表し、それを使うオブジェクトが選びます。Command は Receiver とパラメータを含む特定のリクエストを表し、後で実行したり、キューに入れたり、取り消したりできます。

コールバック関数はコマンドですか?

コールバックは最小限のコマンドです。後で実行する処理をパッケージ化しています。Undo、説明、シリアライズ、その他のメタデータも必要なときには、本格的なコマンドオブジェクトが必要になります。

マクロコマンドとは何ですか?

マクロコマンドはコマンドのリストを保持し、順番に実行します。その Undo は逆の順序で元に戻します。これは Composite パターンの応用です。

コーディング講座の一覧