勉強日記

チラ裏

GoF本 Command

ねらい

  • リクエストをオブジェクトとしてカプセル化
    • クライアントを異なるリクエストでパラメタライズできるようにする
    • リクエストのキューイング・ロギングを可能にする
    • アンドゥのサポートを可能にする

AKA

  • Action
  • Transaction

モチベーション

  • リクエストを「とにかく出す」必要に迫られる局面がある
    • リクエストを誰が受信するのかを知らない
    • リクエストを受信したモジュールがどんなことをするかを知らない
  • 例) DOM Events / ボタン
    • アプリケーションは、ボタンのclickイベントのリスナ関数を登録する
    • ボタンはクリックされたら、単にリスナ関数を実行するだけ = リクエストを「とにかく出す」
    • リスナ関数の中に知識がカプセル化されている
      • どのモジュールの
      • どの操作を呼び出す
  • やりたいことは「コールバック関数」
  • しかし「関数」が第一級オブジェクトでない言語では、いろいろ不便
    • 拡張できない
    • 状態をもたせられない
  • 代わりにCommandオブジェクトを使う

つかいどころ

  • コールバック関数のオブジェクト指向的な置き換え
  • リクエストの指定・キューイング・実行のタイミングが異なる
    • ボタンの例でいうと、リスナの登録のタイミングと、ボタンがクリックされるタイミングとは異なる
  • アンドゥをサポートしたい
    • コールバック関数と異なり、Commandオブジェクトは状態をもつことができる
    • 「実行前」の状態を保持することで、アンドゥを実現可能
  • crashから復帰したい
    • Commandオブジェクトにload/store操作を実装し、ディスクに保存しておくことで実現できる

登場人物

  • Command
  • ConcreteCommand
    • 生成時に受信者オブジェクトを受け取り、参照を保持する
    • リクエスト受信者がどの処理を実行するかを定義する
    • Execute()でこれを実行する
  • Client
    • ConcreteCommandを生成する
      • この際、Receiverオブジェクトを指定する
  • Invoker
    • Command::Execute()を呼ぶ人
  • Receiver
    • リクエストに対応して何か処理を行う人

【補】具体例

  • JSの関数オブジェクトはCommandオブジェクトということにする
const listener = function () {
    console.log('hoge');
};

button.addEventListener('click', listener);
  • Command: Function
  • ConcreteCommandオブジェクト: listener
  • Client: 上記コードが書かれているモジュール
  • Invokerオブジェクト: button
  • Receiverオブジェクト: console

buttonconsole.log('hoge')とが疎結合になっている

クライアントコードからの利用

  • クライアントコードはConcreteCommandオブジェクトを生成し、Receiverオブジェクトを指定する
  • InvokerオブジェクトはConcreteCommandオブジェクトを保持する
  • Invokerオブジェクトはcommand.Execute()を実行することでリクエストを出す
    • アンドゥ可能にする場合、その前に状態を保存しておく
  • ConcreteCommandオブジェクトはリクエストを受け、Receiverオブジェクトの処理を実行する

結果

  • InvokerReceiverとの疎結合
  • Commandは関数ポインタ等とは異なり第一級オブジェクトなので、如何様にも拡張できる
    • Compositeにしたり

実装にあたり考えるべきこと

  • Commandにどれだけの知識をもたせるか
    • 両極端
      • 知識が少ない: 処理をreceiverに委譲する
        • receiverと、receiverに行わせたい処理だけを持つ
      • 知識が多い: ConcreteCommand自体に処理を書く
        • receiverをもたない
          • 適切なreceiverがいない
          • receiverが分かり切っている
            • 新しいアプリケーションウィンドウを開く場合など
    • 中くらい
      • receiverを動的に探す
  • アンドゥ・リドゥのサポート
    • Unexecute()といったインタフェースを定義する
    • 1-level アンドゥ: 最後に実行したコマンドを保持すればよい
      • lastCommand->Unexecute() で戻れる
    • 任意回数戻れるアンドゥ: history listが必要
      • これまで実行してきたCommandオブジェクトのリスト
      • Commandはアンドゥで戻るために状態を保持する必要がある
      • ので、history list上のCommandオブジェクトは、一つ一つが独立した状態を持つ必要がある
      • つまり、history listにはコマンドのクローンを登録する必要がある
        • Prototype Pattern
  • アンドゥ・リドゥを繰り返した際に、エラーが蓄積するのを避ける
    • 最初にCommandを実行する際の状態と、リドゥで実行する際の状態とが変わってしまうため
  • アンドゥで巻き戻るための情報の保持
    • Memento Patternをつかうとよい
  • C++のtemplateをつかう
    • 下記条件を満たす場合、templateでCommand<Receiver>を作れる
      • receiverの処理に引数を渡さない
      • アンドゥをサポートしない
      • 【補】要するに、receiver以外の状態をもたない

関連するパターン

  • Composite
    • 小さなCommandを組み立てて大きなCommandをつくる
  • Memento
    • アンドゥで巻き戻すための、「コマンド実行前の状態」を保持
  • Prototype
    • history listに登録されるCommandオブジェクトは独立したクローンである必要がある。これすなわちPrototype Patternである