Command
一言でいうと
Command (コマンド、 命令) は、 振る舞いに関するデザインパターンの一つで、 リクエストを、 それに関するすべての情報を含む独立したオブジェクトに転換します。 この転換により、 リクエストをメソッドの引数として渡したり、 リクエストの実行を遅らせたり、 待ち行列に入れたり、 取り消し操作を行なうことが可能になります。
問題
新しいテキスト・エディターのアプリを開発しているとします。 今取り組んでいるのは、 エディターの様々な操作のためのボタンをたくさん含むツールバーを作成することです。 ツールバー上のボタンにでも各種ダイアログ上のボタンにでも使用可能な、 ちょっと気の利いた Button
クラスができました。
これらのボタンはすべて似たように見えますが、 それぞれ異なることを行うことになっています。 これらのボタンの様々なクリック処理コードはどこに置けばいいでしょうか? 最も単純な解決策は、 ボタンが使用される場所に応じてサブクラスを作成し、 そこにボタンのクリック時に実行すべきコードを含めるようにすることです。 多数のサブクラスを作成することになります。
やがて、 あなたはこのやり方には大きな欠陥があることに気づきます。 まず膨大な数のサブクラスがあります。 基底の Button
クラスを変更するたびに、 サブクラスが壊れるリスクを心配していなのでしたら、 それでも大丈夫なのですが。 簡単に言うと、 GUI コードは、 変更が常のビジネス・ロジックのコードにいつの間にか依存するようになってしまった、 ということです。
そしてこれが一番の問題です。 テキストのコピーや貼り付けなどのいくつかの操作は、 複数の場所から呼び出される必要があります。 たとえば、 ツールバー上の小さな 「コピー」 ボタンをクリックすることも、 コンテキスト・メニューからコピーをすることも、 キーボードで Ctrl+C
を押すこともできます。
当初、 私たちのアプリにはツールバーしかなかった時、 ボタンのサブクラス内に様々な操作の実装を置くことには何も問題ありませんでした。 つまり、 CopyButton
のサブクラス内にテキストをコピーするためのコードがあっても大丈夫でした。 しかし次にコンテキスト・メニューやショートカットなどを実装すると、 多くのクラスで操作のコードを重複させるか、 メニューをボタンに依存させるというさらに悪い選択肢を選ばざるを得なくなります。
解決策
良いソフトウェアの設計は、 多くの場合、 単一責任の原則に基づいており、 これは通常アプリをいくつかの層に分割するに至ります。 最も一般的な例としては、 グラフィカル・ユーザー・インターフェース (GUI) 用のレイヤーと、 ビジネス・ロジック用のレイヤーがあります。 GUI 層は、 画面に美しい絵を描画し、 あらゆる入力を取り込み、 ユーザーとアプリが行っていることの結果を表示する役割を担っています。 しかし、 GUI 層は、 月の軌道の計算とか年次報告書の作成のような重要な仕事をビジネス・ロジック層に委ねます。
コード上では、 こんな感じです: GUI オブジェクトは、 いくつかの引数を渡して、 ビジネス・ロジックのオブジェクトのメソッドを呼び出す。 このことは通常、 「あるオブジェクトから別のオブジェクトにリクエストを送る」 と表現されます。
Command パターンに従うと、 GUI オブジェクトは、 このようなリクエストを直接送るべきではありません。 代わりに、 呼び出し対象のオブジェクト、 メソッド名、 引数などのリクエストの詳細を Command クラスに抽出します。 このクラスには、 リクエストの引き金となるようなメソッドが一つだけあります。
コマンドオ・ブジェクトは、 様々な GUI とビジネス・ロジックのオブジェクトとの間のリンクとして機能します。 これ以降、 GUI オブジェクトは、 どのビジネス・ロジックのオブジェクトがリクエストを受け取り、 どう処理するのかを知る必要はありません。 GUI オブジェクトが、 コマンドの引き金を引くだけで、 細かいことはコマンドが処理します。
次のステップは、 コマンドが同じインターフェースを実装するようにすることです。 このインターフェースは、 通常はパラメーターなしの単一の実行メソッドを持っています。 このおかげで、 コマンドの特定の具象クラスに密結合せずに、 様々なコマンドを使用できるようになります。 ボーナスとして、 送信オブジェクトにリンクされているコマンド・オブジェクトを変更できるので、 送信オブジェクトの振る舞いを動的に変更するのと同じ効果があります。
一つ欠けているものがあるのにお気づきですか? リクエストのパラメーターです。 ある GUI オブジェクトは、 ビジネス層のオブジェクトに何らかのパラメーターを渡していたかもしれません。 コマンドの実行メソッドには引数がありません。 どうやってリクエストの細かいことを受け手に渡せばいいのでしょう? このため、 コマンドは、 データを事前構成しておくか、 構成を自前で取得する能力を持っている必要があります。
テキスト・エディターの話に戻りましょう。 Command パターンを適用した後、 様々なクリック動作を実装するためのボタンのサブクラスはすべて不要となりました。 基底クラスである Button
クラスにコマンド・オブジェクトへの参照を格納するフィールド一つを追加し、 クリックに応じてボタンがそのコマンドを実行するだけで十分です。
すべての可能な操作に対してコマンドのクラスを多数実装し、 ボタンの意図した振る舞いに応じて特定のボタンとリンクします。
メニュー、 ショートカット、 ダイアログ全体等、 他の GUI 要素も同様に実装できます。 ユーザーがある GUI 要素を操作すると、 それにリンクされたコマンドが実行されます。 多分もうお分かりかと思いますが、 同じ操作に関する GUI 要素は同じコマンドにリンクされ、 コードの重複を防ぎます。
結果として、 コマンドは、 GUI とビジネス・ロジック層との結合を減らす便利な中間層となります。 そしてこれは、 Command パターンがもたらすメリットのほんの一部に過ぎません!
現実世界でのたとえ
結構長い時間かけて街中を歩き回った後、 やっと素敵なレストランの窓際のテーブルにありつけました。 フレンドリーなウェイターがやってきて、 素早く注文を取り、 紙の上に書き留めます。 ウェイターはキッチンへ行き、 壁に注文票を貼り付けます。 しばらくすると、 注文票はシェフのところにたどり着き、 シェフはそれを読み、 その通りに調理します。 料理人は注文票と一緒にトレイに食事を置きます。 ウェイターはトレイを見つけ、 注文通りにできていることを確認してから、 すべてをテーブルに運びます。
紙の注文票は、 コマンドの働きをしています。 それは、 シェフが注文の処理ができるまで、 待ち行列に入ります。 注文票には、 調理に必要な関連情報がすべて含まれています。 シェフは、 客の注文が何であったかを明らかにするために、 あちこち走り回ることなく、 すぐに調理を開始できます。
構造
-
送り手 (Sender) クラス、 別名インボーカー (Invoker) は、 リクエストの開始を担当します。 このクラスには、 コマンドオ・ブジェクトへの参照を保存するためのフィールドが必要となります。 送り手は、 リクエストを受け手に直接送る代わりに、 コマンドの引き金を引きます。 送り手にはコマンド・オブジェクトの作成の責任がないことに注目してください。 通常は、 クライアントからコンストラクターを介して事前に作成されたコマンドを取得します。
-
コマンド (Command) インターフェースは通常、 コマンドを実行するためのメソッドを一つ宣言します。
-
具象コマンド (Concrete Command) は様々な種類のリクエストを実装します。 具象コマンドは、 仕事を独立して自身で行うべきではなく、 ビジネス・ロジックのオブジェクトのどれかに仕事を渡すべきです。 しかし、 コードの簡素化のためにこの二つのクラスの統合は可能です。
受け手のオブジェクトでメソッドの実行に必要なパラメーターは、 具象コマンドのフィールドとして宣言することができます。 これらのフィールドの初期化をコンストラクター内だけで許可するようにすれば、 コマンド・オブジェクトは不変となります。
-
受け手 (Receiver) クラスには、 何らかのビジネス・ロジックが含まれています。 ほとんどどんなオブジェクトでも受け手になれます。 ほとんどのコマンドは、 リクエストを受け手に渡す詳細を扱い、 実際の作業は受け手が行います。
-
クライアント (Client) は、 具象コマンド・オブジェクトの作成および構成を行います。 クライアントは、 受け手インスタンスを含むすべてのリクエスト・パラメータをコマンドのコンストラクターに渡す必要があります。 その後、 作成されたコマンドは、 一つ以上の送り手に関連付けられます。
擬似コード
この例では、 Command パターンが、 実行された操作の履歴を記録し、 必要に応じて操作の取り消しを可能にするために役立っています。
エディターの状態の変更に至るコマンド (例: 切り取りとペースト) は、 コマンドに関連する操作を実行する前に、 エディターの状態のバックアップ用コピーを作成します。 コマンド実行後に、 コマンド・オブジェクトはコマンド履歴 (コマンド・オブジェクトのスタック) に、 その時点でエディターの状態のバックアップ用コピーとともに置かれます。 その後、 ユーザーが操作を取り消す必要がある場合は、 アプリは履歴から最新のコマンドを取り出し、 それに関連したエディターの状態のバックアップを読み込んで、 復元を行います。
クライアント・コード (GUI 要素、 コマンド履歴など) は、 コマンド・インターフェースを介してコマンドと連携協働するため、 どんなコマンドの具象クラスとも連携していません。 このやり方では、 既存のコードを壊すことなく、 新しいコマンドをアプリに導入できます。
適応性
操作をオブジェクトを使いパラメーターとして扱うためには、 Command パターンを使用します。
Command パターンは、 特定のメソッドの呼び出しを独立したオブジェクトに変えることができます。 この変更により、 以下のような多くの興味深い用途が考えられます: コマンドをメソッドの引数として渡す。 他のオブジェクトに格納する。 リンクされたコマンドを実行時に切り替えるなど。
例を示します。 コンテキストメニューのような GUI コンポーネントを開発しているとします。 そして、 エンド・ユーザーがメニュー項目をクリックした時に、 どのような操作が起動されるかの設定を管理者ができるようにしたいとします。
操作を待ち行列に入れたり、 実行をスケジュールしたり、 リモートで実行したい場合は、 Command パターンを使用します。
他のオブジェクト同様、 コマンドもシリアライズ (直列化) することができ、 文字列に変換してファイルやデータベースに簡単に書き込めます。 その後、 文字列を元のコマンド・オブジェクトに復元できます。 したがって、 コマンドの実行を遅延し、 後ほどスケジュールに従って実行することができます。 しかし、 それだけではありません! 同様にして、 待ち行列に入れたり、 ログに記録したり、 ネットワークを通してコマンドを送信したりもできます。
取り消し可能な操作の実装にも、 Command パターンが使えます。
取り消しと再実行 (undo/redo) の実装方法にはいろいろありますが、 Command パターンはおそらく最もよく使われている方法です。
操作の取り消しを可能とするには、 実行された操作履歴の実装が必要となります。 コマンド履歴は、 実行されたすべてのコマンド・オブジェクトとそれに関連するアプリケーションの状態のバックアップを含んだスタックです。
このメソッドには二つの欠点があります。 最初に、 アプリケーションの状態の保存は、 その一部が非公開である可能性があるので、 それほど簡単ではありません。 この問題は Memento パターンで、 軽減できます。
二番目は、 状態バックアップはかなり多くの RAM を消費するかもしれないということです。 したがって、 時には、 過去の状態を復元する代わりに、 コマンドは逆操作を実行する、 という代替実装に頼る必要があります。 逆操作にも問題があります。 実装が結果的に極めて困難であったり、 不可能であるという結論に至る可能性があります。
実装方法
-
実行メソッドを一つだけ持つコマンド・インターフェースを宣言します。
-
コマンド・インターフェースを実装する具象コマンド・クラスにリクエストを抽出します。 各クラスは、 リクエストの引数と、 実際の受け取りオブジェクトへの参照を格納するためのフィールドをいくつか持っている必要があります。 これら値はすべて、 コマンドのコンストラクターを通して初期化されなければなりません。
-
送り手として機能するクラスをいくつか探し出します。 これらのクラスにコマンドを保存するためのフィールドを追加します。 送り手は、 コマンド・インターフェースを通してのみコマンドと通信する必要があります。 送り手は通常、 自身でコマンド・オブジェクトを作成することはせず、 クライアント・コードから取得します。
-
送り手を変更して、 直接リクエストを受け手に送る代わりに、 コマンドを実行するようにします。
-
クライアントは以下の順序でオブジェクトを初期化します:
- 受け手を作成。
- コマンドを作成し、 必要に応じて受け手と関連付ける。
- 送り手を作成し、 特定のコマンドに関連付ける。
長所と短所
- 単一責任の原則。 処理を起動するクラスを、 実際に処理をするクラスから分離可能。
- 開放閉鎖の原則。 新規コマンドをアプリに導入しても、 既存クライアント側コードは問題なく動作する。
- 取り消しと再実行を実装可能。
- 操作の遅延実行を実装可能。
- 単純なコマンドを束ねて複雑なコマンドの作成可能。
- 送り手と受け手の間で新しい層を導入するため、 コードが複雑化する可能性。
他のパターンとの関係
-
Chain of Responsibility と Command と Mediator と Observer は、 リクエストの送り手と受け手を接続する様々な方法を示します:
- Chain of Responsibility は、 潜在的受け手の動的な連鎖に沿って、 どれか一つが処理するまで、 リクエストを順番に渡します。
- Command は、 送り手と受け手との間で単方向の接続を確立します。
- Mediator は、 送り手と受け手の間の直接の接続を削除し、 メディエーター・オブジェクトを介しての間接的通信を強制します。
- Observer では、 受け手が動的にリクエストの受信申し込みをしたり、 申し込み取り消しをしたりできます。
-
Chain of Responsibility のハンドラーは、 Commands で実装可能です。 この場合、 リクエストに代表される同一のコンテキスト・オブジェクトに対して多くの異なる処理を実行できます。
しかしもう一つのやり方は、 リクエスト自身をコマンド・オブジェクトとすることです。 この場合、 連鎖にリンクされた異なる一連のコンテキスト中で同じ処理を実行できます。
-
Command と Memento とを一緒に使用して、 「取り消す」 を実装可能です。 この場合、 コマンドは、 一つのターゲット・オブジェクトに対して異なる操作を実行する責任を負い、 メメントは、 コマンド実行前にオブジェクトの状態を保存します。
-
Command と Strategy は、 オブジェクトを何らかの操作でパラメーター化できるため、 似たように見えます。 しかし、 この二つはまったく異なる意図を持っています。
-
Command を使用して、 任意の操作をオブジェクトに変換できます。 操作のパラメーターは、 そのオブジェクトのフィールドになります。 変換により、 操作の実行を延期したり、 キューに入れたり、 コマンド履歴を保存したり、 遠隔サービスにコマンドを送信したりできます。
-
一方、 Strategy は通常、 同じことを行う異なる方法に関するものです。 単一のコンテキスト・クラス内でアルゴリズムを入れ替えることができます。
-
-
Visitor を Command パターンのより強力なものとして扱うことができます。 そのオブジェクトは、 異なるクラスの様々なオブジェクトに対して操作を実行できます。