春のセール

Command

別名:Action、Transaction、コマンド、命令、アクション、トランザクション

一言でいうと

Command コマンド 命令 振る舞いに関するデザインパターンの一つで リクエストを それに関するすべての情報を含む独立したオブジェクトに転換します この転換により リクエストをメソッドの引数として渡したり リクエストの実行を遅らせたり 待ち行列に入れたり 取り消し操作を行なうことが可能になります

Command デザインパターン

問題

新しいテキスト・エディターのアプリを開発しているとします 今取り組んでいるのは エディターの様々な操作のためのボタンをたくさん含むツールバーを作成することです ツールバー上のボタンにでも各種ダイアログ上のボタンにでも使用可能な ちょっと気の利いた Button クラスができました

Command パターンによって解決された問題

アプリのすべてのボタンは 同じクラスから派生する

これらのボタンはすべて似たように見えますが それぞれ異なることを行うことになっています これらのボタンの様々なクリック処理コードはどこに置けばいいでしょうか 最も単純な解決策は ボタンが使用される場所に応じてサブクラスを作成し そこにボタンのクリック時に実行すべきコードを含めるようにすることです 多数のサブクラスを作成することになります

多数のボタンのサブクラス

多くのサブクラス はて 何か問題ある

やがて あなたはこのやり方には大きな欠陥があることに気づきます まず膨大な数のサブクラスがあります 基底の Button クラスを変更するたびに サブクラスが壊れるリスクを心配していなのでしたら それでも大丈夫なのですが 簡単に言うと GUI コードは 変更が常のビジネス・ロジックのコードにいつの間にか依存するようになってしまった ということです

いくつかのクラスが同じ機能を実装

いくつかのクラスが同じ機能を実装

そしてこれが一番の問題です テキストのコピーや貼り付けなどのいくつかの操作は 複数の場所から呼び出される必要があります たとえば ツールバー上の小さな コピー ボタンをクリックすることも コンテキスト・メニューからコピーをすることも キーボードで Ctrl+C を押すこともできます

当初 私たちのアプリにはツールバーしかなかった時 ボタンのサブクラス内に様々な操作の実装を置くことには何も問題ありませんでした つまり Copy­Button のサブクラス内にテキストをコピーするためのコードがあっても大丈夫でした しかし次にコンテキスト・メニューやショートカットなどを実装すると 多くのクラスで操作のコードを重複させるか メニューをボタンに依存させるというさらに悪い選択肢を選ばざるを得なくなります

解決策

良いソフトウェアの設計は 多くの場合 に基づいており これは通常アプリをいくつかの層に分割するに至ります 最も一般的な例としては グラフィカル・ユーザー・インターフェース GUI 用のレイヤーと ビジネス・ロジック用のレイヤーがあります GUI 層は 画面に美しい絵を描画し あらゆる入力を取り込み ユーザーとアプリが行っていることの結果を表示する役割を担っています しかし GUI 層は 月の軌道の計算とか年次報告書の作成のような重要な仕事をビジネス・ロジック層に委ねます

コード上では こんな感じです GUI オブジェクトは いくつかの引数を渡して ビジネス・ロジックのオブジェクトのメソッドを呼び出す このことは通常 あるオブジェクトから別のオブジェクトにを送る と表現されます

GUI 層に、ビジネス層オブジェクトへの直接アクセスを許す

GUI オブジェクトに ビジネス・ロジックのオブジェクトへの直接アクセスを許す

Command パターンに従うと GUI オブジェクトは このようなリクエストを直接送るべきではありません 代わりに 呼び出し対象のオブジェクト メソッド名 引数などのリクエストの詳細を Command クラスに抽出します このクラスには リクエストの引き金となるようなメソッドが一つだけあります

コマンドオ・ブジェクトは 様々な GUI とビジネス・ロジックのオブジェクトとの間のリンクとして機能します これ以降 GUI オブジェクトは どのビジネス・ロジックのオブジェクトがリクエストを受け取り どう処理するのかを知る必要はありません GUI オブジェクトが コマンドの引き金を引くだけで 細かいことはコマンドが処理します

コマンドを介してビジネス・ロジック層にアクセス。

コマンドを介してビジネス・ロジック層にアクセス

次のステップは コマンドが同じインターフェースを実装するようにすることです このインターフェースは 通常はパラメーターなしの単一の実行メソッドを持っています このおかげで コマンドの特定の具象クラスに密結合せずに 様々なコマンドを使用できるようになります ボーナスとして 送信オブジェクトにリンクされているコマンド・オブジェクトを変更できるので 送信オブジェクトの振る舞いを動的に変更するのと同じ効果があります

一つ欠けているものがあるのにお気づきですか リクエストのパラメーターです ある GUI オブジェクトは ビジネス層のオブジェクトに何らかのパラメーターを渡していたかもしれません コマンドの実行メソッドには引数がありません どうやってリクエストの細かいことを受け手に渡せばいいのでしょう このため コマンドは データを事前構成しておくか 構成を自前で取得する能力を持っている必要があります

GUI オブジェクトは作業をコマンドに委任

GUI オブジェクトは作業をコマンドに委任

テキスト・エディターの話に戻りましょう Command パターンを適用した後 様々なクリック動作を実装するためのボタンのサブクラスはすべて不要となりました 基底クラスである Button クラスにコマンド・オブジェクトへの参照を格納するフィールド一つを追加し クリックに応じてボタンがそのコマンドを実行するだけで十分です

すべての可能な操作に対してコマンドのクラスを多数実装し ボタンの意図した振る舞いに応じて特定のボタンとリンクします

メニュー ショートカット ダイアログ全体等 他の GUI 要素も同様に実装できます ユーザーがある GUI 要素を操作すると それにリンクされたコマンドが実行されます 多分もうお分かりかと思いますが 同じ操作に関する GUI 要素は同じコマンドにリンクされ コードの重複を防ぎます

結果として コマンドは GUI とビジネス・ロジック層との結合を減らす便利な中間層となります そしてこれは Command パターンがもたらすメリットのほんの一部に過ぎません

現実世界でのたとえ

レストランで注文票を作成

レストランで注文票を作成

結構長い時間かけて街中を歩き回った後 やっと素敵なレストランの窓際のテーブルにありつけました フレンドリーなウェイターがやってきて 素早く注文を取り 紙の上に書き留めます ウェイターはキッチンへ行き 壁に注文票を貼り付けます しばらくすると 注文票はシェフのところにたどり着き シェフはそれを読み その通りに調理します 料理人は注文票と一緒にトレイに食事を置きます ウェイターはトレイを見つけ 注文通りにできていることを確認してから すべてをテーブルに運びます

紙の注文票は コマンドの働きをしています それは シェフが注文の処理ができるまで 待ち行列に入ります 注文票には 調理に必要な関連情報がすべて含まれています シェフは 客の注文が何であったかを明らかにするために あちこち走り回ることなく すぐに調理を開始できます

構造

Command デザインパターンの構造Command デザインパターンの構造
  1. 送り手 Sender クラス 別名 Invoker リクエストの開始を担当します このクラスには コマンドオ・ブジェクトへの参照を保存するためのフィールドが必要となります 送り手は リクエストを受け手に直接送る代わりに コマンドの引き金を引きます 送り手にはコマンド・オブジェクトの作成の責任がないことに注目してください 通常は クライアントからコンストラクターを介して事前に作成されたコマンドを取得します

  2. コマンド Command インターフェースは通常 コマンドを実行するためのメソッドを一つ宣言します

  3. 具象コマンド Concrete Command は様々な種類のリクエストを実装します 具象コマンドは 仕事を独立して自身で行うべきではなく ビジネス・ロジックのオブジェクトのどれかに仕事を渡すべきです しかし コードの簡素化のためにこの二つのクラスの統合は可能です

    受け手のオブジェクトでメソッドの実行に必要なパラメーターは 具象コマンドのフィールドとして宣言することができます これらのフィールドの初期化をコンストラクター内だけで許可するようにすれば コマンド・オブジェクトは不変となります

  4. 受け手 Receiver クラスには 何らかのビジネス・ロジックが含まれています ほとんどどんなオブジェクトでも受け手になれます ほとんどのコマンドは リクエストを受け手に渡す詳細を扱い 実際の作業は受け手が行います

  5. クライアント Client 具象コマンド・オブジェクトの作成および構成を行います クライアントは 受け手インスタンスを含むすべてのリクエスト・パラメータをコマンドのコンストラクターに渡す必要があります その後 作成されたコマンドは 一つ以上の送り手に関連付けられます

擬似コード

この例では Command パターンが 実行された操作の履歴を記録し 必要に応じて操作の取り消しを可能にするために役立っています

Command パターン例の構造

テキスト・エディターの取り消し可能な操作

エディターの状態の変更に至るコマンド 切り取りとペースト コマンドに関連する操作を実行する前に エディターの状態のバックアップ用コピーを作成します コマンド実行後に コマンド・オブジェクトはコマンド履歴 コマンド・オブジェクトのスタック その時点でエディターの状態のバックアップ用コピーとともに置かれます その後 ユーザーが操作を取り消す必要がある場合は アプリは履歴から最新のコマンドを取り出し それに関連したエディターの状態のバックアップを読み込んで 復元を行います

クライアント・コード GUI 要素 コマンド履歴など コマンド・インターフェースを介してコマンドと連携協働するため どんなコマンドの具象クラスとも連携していません このやり方では 既存のコードを壊すことなく 新しいコマンドをアプリに導入できます

// コマンドの基底クラスは、すべての具象コマンドに共通なインターフェースを
// 定義。
abstract class Command is
    protected field app: Application
    protected field editor: Editor
    protected field backup: text

    constructor Command(app: Application, editor: Editor) is
        this.app = app
        this.editor = editor

    // エディターの状態のバックアップを作成。
    method saveBackup() is
        backup = editor.text

    // エディターの状態を復元。
    method undo() is
        editor.text = backup

    // 実行メソッドを abstract と宣言することにより、すべての具象コマン
    // ドが専用の実装を行うことを強制。メソッドは、コマンドがエディターの
    // 状態を変更したかどうかに応じて、true または false を返す。
    abstract method execute()


// 具象コマンドはここで定義。
class CopyCommand extends Command is
    // コピー・コマンドはエディターの状態を変更しないので、履歴には保存し
    // ない。
    method execute() is
        app.clipboard = editor.getSelection()
        return false

class CutCommand extends Command is
    // 切り取りコマンドはエディターの状態を変更するので、履歴に保存する必
    // 要あり。メソッドが true を返す限り、保存される。
    method execute() is
        saveBackup()
        app.clipboard = editor.getSelection()
        editor.deleteSelection()
        return true

class PasteCommand extends Command is
    method execute() is
        saveBackup()
        editor.replaceSelection(app.clipboard)
        return true

// 取り消し操作もコマンドの一つ。
class UndoCommand extends Command is
    method execute() is
        app.undo()
        return false


// 大域コマンド履歴は、単なるスタック。
class CommandHistory is
    private field history: array of Command

    // 最後に入れたものを……
    method push(c: Command) is
        // コマンドを履歴配列の後方にプッシュ。

    // ……最初に取り出す。
    method pop():Command is
        // 履歴から最新のコマンドを取得。


// エディター・クラスは、実際のエディターの操作を保持。受け手の役を果たし、
// 全コマンドは、実行をエディターのメソッドに委任。
class Editor is
    field text: string

    method getSelection() is
        // 選択されたテキストを返す。

    method deleteSelection() is
        // 選択されたテキストを削除。

    method replaceSelection(text) is
        // クリップボードの内容を現在の位置に貼り付ける。


// アプリケーション・クラスは、オブジェクト同士の関係を設定する。何かを行
// う必要がある時に送り手として機能し、コマンド・オブジェクトを作成して実
// 行。
class Application is
    field clipboard: string
    field editors: array of Editors
    field activeEditor: Editor
    field history: CommandHistory

    // UI のオブジェクトにコマンドを割り当てるコードは、こんな感じ。
    method createUI() is
        // ……
        copy = function() { executeCommand(
            new CopyCommand(this, activeEditor)) }
        copyButton.setCommand(copy)
        shortcuts.onKeyPress("Ctrl+C", copy)

        cut = function() { executeCommand(
            new CutCommand(this, activeEditor)) }
        cutButton.setCommand(cut)
        shortcuts.onKeyPress("Ctrl+X", cut)

        paste = function() { executeCommand(
            new PasteCommand(this, activeEditor)) }
        pasteButton.setCommand(paste)
        shortcuts.onKeyPress("Ctrl+V", paste)

        undo = function() { executeCommand(
            new UndoCommand(this, activeEditor)) }
        undoButton.setCommand(undo)
        shortcuts.onKeyPress("Ctrl+Z", undo)

    // コマンドを実行し、それが履歴に追加されたかどうかをチェック。
    method executeCommand(command) is
        if (command.execute())
            history.push(command)

    // 履歴から最新のコマンドを取り、取り消しメソッドを実行。そのコマンド
    // のクラスが不明であることに注意。コマンドはそれ自身の操作を取り消す
    // 方法を周知しているため、知る必要なし。
    method undo() is
        command = history.pop()
        if (command != null)
            command.undo()

適応性

操作をオブジェクトを使いパラメーターとして扱うためには Command パターンを使用します

Command パターンは 特定のメソッドの呼び出しを独立したオブジェクトに変えることができます この変更により 以下のような多くの興味深い用途が考えられます コマンドをメソッドの引数として渡す 他のオブジェクトに格納する リンクされたコマンドを実行時に切り替えるなど

例を示します コンテキストメニューのような GUI コンポーネントを開発しているとします そして エンド・ユーザーがメニュー項目をクリックした時に どのような操作が起動されるかの設定を管理者ができるようにしたいとします

操作を待ち行列に入れたり 実行をスケジュールしたり リモートで実行したい場合は Command パターンを使用します

他のオブジェクト同様 コマンドもシリアライズ 直列化 することができ 文字列に変換してファイルやデータベースに簡単に書き込めます その後 文字列を元のコマンド・オブジェクトに復元できます したがって コマンドの実行を遅延し 後ほどスケジュールに従って実行することができます しかし それだけではありません 同様にして 待ち行列に入れたり ログに記録したり ネットワークを通してコマンドを送信したりもできます

取り消し可能な操作の実装にも Command パターンが使えます

取り消しと再実行 undo/redo の実装方法にはいろいろありますが Command パターンはおそらく最もよく使われている方法です

操作の取り消しを可能とするには 実行された操作履歴の実装が必要となります コマンド履歴は 実行されたすべてのコマンド・オブジェクトとそれに関連するアプリケーションの状態のバックアップを含んだスタックです

このメソッドには二つの欠点があります 最初に アプリケーションの状態の保存は その一部が非公開である可能性があるので それほど簡単ではありません この問題は Memento パターンで 軽減できます

二番目は 状態バックアップはかなり多くの RAM を消費するかもしれないということです したがって 時には 過去の状態を復元する代わりに コマンドは逆操作を実行する という代替実装に頼る必要があります 逆操作にも問題があります 実装が結果的に極めて困難であったり 不可能であるという結論に至る可能性があります

実装方法

  1. 実行メソッドを一つだけ持つコマンド・インターフェースを宣言します

  2. コマンド・インターフェースを実装する具象コマンド・クラスにリクエストを抽出します 各クラスは リクエストの引数と 実際の受け取りオブジェクトへの参照を格納するためのフィールドをいくつか持っている必要があります これら値はすべて コマンドのコンストラクターを通して初期化されなければなりません

  3. として機能するクラスをいくつか探し出します これらのクラスにコマンドを保存するためのフィールドを追加します 送り手は コマンド・インターフェースを通してのみコマンドと通信する必要があります 送り手は通常 自身でコマンド・オブジェクトを作成することはせず クライアント・コードから取得します

  4. 送り手を変更して 直接リクエストを受け手に送る代わりに コマンドを実行するようにします

  5. クライアントは以下の順序でオブジェクトを初期化します

    • 受け手を作成
    • コマンドを作成し 必要に応じて受け手と関連付ける
    • 送り手を作成し 特定のコマンドに関連付ける

長所と短所

  • 処理を起動するクラスを 実際に処理をするクラスから分離可能
  • 新規コマンドをアプリに導入しても 既存クライアント側コードは問題なく動作する
  • 取り消しと再実行を実装可能
  • 操作の遅延実行を実装可能
  • 単純なコマンドを束ねて複雑なコマンドの作成可能
  • 送り手と受け手の間で新しい層を導入するため コードが複雑化する可能性

他のパターンとの関係

  • Chain of ResponsibilityCommandMediatorObserver リクエストの送り手と受け手を接続する様々な方法を示します

    • Chain of Responsibility 潜在的受け手の動的な連鎖に沿って どれか一つが処理するまで リクエストを順番に渡します
    • Command 送り手と受け手との間で単方向の接続を確立します
    • Mediator 送り手と受け手の間の直接の接続を削除し メディエーター・オブジェクトを介しての間接的通信を強制します
    • Observer では 受け手が動的にリクエストの受信申し込みをしたり 申し込み取り消しをしたりできます
  • Chain of Responsibility のハンドラーは Commands で実装可能です この場合 リクエストに代表される同一のコンテキスト・オブジェクトに対して多くの異なる処理を実行できます

    しかしもう一つのやり方は リクエスト自身を・オブジェクトとすることです この場合 連鎖にリンクされた異なる一連のコンテキスト中で同じ処理を実行できます

  • CommandMemento とを一緒に使用して 取り消す を実装可能です この場合 コマンドは 一つのターゲット・オブジェクトに対して異なる操作を実行する責任を負い メメントは コマンド実行前にオブジェクトの状態を保存します

  • CommandStrategy オブジェクトを何らかの操作でパラメーター化できるため 似たように見えます しかし この二つはまったく異なる意図を持っています

    • Command を使用して 任意の操作をオブジェクトに変換できます 操作のパラメーターは そのオブジェクトのフィールドになります 変換により 操作の実行を延期したり キューに入れたり コマンド履歴を保存したり 遠隔サービスにコマンドを送信したりできます

    • 一方 Strategy は通常 同じことを行う異なる方法に関するものです 単一のコンテキスト・クラス内でアルゴリズムを入れ替えることができます

  • Prototype Commands のコピーを履歴に保存する必要がある場合に役立ちます

  • VisitorCommand パターンのより強力なものとして扱うことができます そのオブジェクトは 異なるクラスの様々なオブジェクトに対して操作を実行できます

コード例

Command を C# で Command を C++ で Command を Go で Command を Java で Command を PHP で Command を Python で Command を Ruby で Command を Rust で Command を Swift で Command を TypeScript で