イベントコラボレーション
複数のコンポーネントは、内部状態が変化したときにイベントを送信することで互いに通信し、連携して動作します。
2006年6月19日
これは、2000年代半ばに執筆していた「Further Enterprise Application Architecture development(エンタープライズアプリケーションアーキテクチャ開発のさらなる発展)」の一部です。残念ながら、それ以来、他の多くのことが私の注意を引くようになり、それらをさらに進める時間がありませんでしたし、近い将来にも時間が見込めません。そのため、この資料は非常に草稿の状態であり、再び作業する時間を見つけるまで、修正や更新は行いません。
コンポーネントが互いに連携する場合、それらが単一のアドレス空間内の小さなオブジェクトであっても、インターネットを介して通信するアプリケーションのように大きくても、通常、連携のスタイルはリクエストによって駆動されると考えます。あるコンポーネントが別のコンポーネントにある情報を必要とする場合、必要とするコンポーネントはそれをリクエストします。そして、そのコンポーネントが別のコンポーネントに何かをさせる必要がある場合は、別のリクエストが送信されます。
イベントコラボレーションは異なった動作をします。コンポーネントが必要なときにリクエストを行う代わりに、コンポーネントは変更が発生したときにイベントを発生させます。他のコンポーネントはイベントをリスンし、適切に反応します。イベントコラボレーションは、各部分が他の部分との相互作用について考える方法に、非常に異なる考え方をもたらします。
仕組み
多くの場合と同様に、始めるのに最も簡単な場所は例を示すことです。2006年の私の家を、おそらく70年代に想像されたであろう形で設定してみましょう。そのため、私が外出したいときは、Zenのようなばかげた名前を持つ家のコンピューターに指示します。コンピューターは外気温を考慮し、ロボット執事に寒いニューイングランドの冬の日にダウンジャケットを持って来るように指示します。

図1:リクエストを使用したコラボレーション
シーケンス図は、その違いをよく示しています。図1は、リクエストコラボレーションスタイルを使用しています。私がZenに外出すると伝えると、Zenは温度センサーに温度を問い合わせ、この情報を使用して必要なコートの種類を判断し、執事にダウンジャケットを持ってくるように指示します。

図2:イベントを使用したコラボレーション
ここでのコラボレーションには2つの要素があります。Zenが温度センサーに発行するクエリと、Zenが執事に発行するコマンドです。クエリとコマンドは異なり(ただし組み合わせることもできます)、通信パターンに異なる影響を与えます。
図2のイベントコラボレーションは異なった動作をします。この例には、著者の単純化といういつもの匂いがしますが、これら2つのスタイルの重要な違いのいくつかを明らかにするのに役立つと思います。
1つの明らかな違いは、イベントが反応するコンポーネントだけでなく、他のすべてのコンポーネントにも送信されることです。ここでのポイントは、送信者はイベントをブロードキャストするだけで、誰が関心を持っているか、誰が応答するかを知る必要がないということです。この疎結合により、送信者は応答を気にする必要がなくなり、イベントバスに新しいコンポーネントをプラグインすることで動作を追加できます。この柔軟性には、後述するように、長所と短所があります。
コマンド
コラボレーションスタイルの違いは、コマンドとクエリで異なる方法で現れるため、それらを別々に、逆の順序で見ていきます。多くの点で、コマンドは2つの相互作用のうち変更が少ない方で、執事に必要なことをさせるように指示する必要があります。1つの違いは、イベントの名前の付け方です。受信者に何かをするように指示する形式で表現するのではなく、イベントが発生したことを伝える形式を取り、関心のある人が応答するように促します。これは微妙な違いであり、私の母がコマンドを発行する方法を思い起こさせますが、そこには何かがあります。時には、この区別は単なる言い回し以上のものですが、他の時には、物事を行うための異なる方法に心を開きます。
イベントの「未知の」受信者という側面がここで問題になります。執事がシンディからアンティークのライブラリチェアの壊れた脚を交換するように言われた場合はどうなるでしょうか。執事は彼女の命令が私の命令よりもはるかに重要であることを十分に理解していると思いますが、Zenはこれにどのように反応するのでしょうか。たとえば、タスクを受け入れてタスクを完了するためのイベントを使用して、プロトコルを追加することができます。いずれにせよ、Zenがジョブが完了したことを本当に確認したい場合は、誰かがコマンドの実行に同意したことを確認する必要があるという暗黙の責任があります。イベントを発生させるだけではコマンドにはなりません。何かが動作することを知っている必要があります。
関連する問題は、2人目の執事を雇った場合に発生します。どちらがイベントに応答する必要がありますか?真の分散型イベント駆動の世界では、執事同士でそれを理解すると想定しますが、私が「ありがとう」と言うとプログラマーが受け取る喜びの衝撃に必死になって、私の貧しいジャケットをめぐって彼らが戦っているという考えを私は取り除くことができません。
イベントコラボレーションの世界でさえ、誰がコマンドを実行するかを集中管理する役割があります。そのため、Zenは「執事ジョージがマーティンのジャケットを取得するという要望が発生しました」というイベントをブロードキャストする可能性があります。私の母はこれをあまりにも直接的すぎると感じるでしょう。実際には、これはイベントとコマンドの違いが霧の中に簡単に消えてしまうことを意味します。それが常にそうであるとは限らないことを覚えておいてください。
クエリ
より興味深い変化はクエリで発生します。イベントコラボレーションの場合、Zenは温度センサーに温度を要求しないことに気付くでしょう。代わりに、温度センサーが温度をブロードキャストします。通常、この種のブロードキャストは温度が変化したときに行われますが、定期的なスケジュールで行われる場合もあります。
温度がブロードキャストされると、そのデータを必要とするコンポーネントがそれを記録するという考え方です。私がZenに外出すると伝えると、Zenは最新のイベントを記録しており、温度を知っているので、温度センサーに温度を尋ねません。 Jon Udellを言い換えると、イベントコラボレーションの最も興味深い違いは次のとおりです。リクエスト駆動型ソフトウェアは話しかけられたときに話し、イベント駆動型ソフトウェアは何か言いたいことがあるときに話します。
この結果、状態管理の責任がシフトします。リクエストコラボレーションでは、すべてのデータが1つのホームを持つように努め、必要な場合はそのホームから検索します。このホームは、データの構造、保存期間、アクセス方法を担当します。イベントコラボレーションシナリオでは、新しいデータのソースは、データがメッセージエンドポイントに渡された瞬間にデータを忘れても構いません。
結果として、データはユーザー(この場合はZen)によって保存する必要があります。Zenが過去の気温の記録を必要とする場合(たとえば、私が外出するときに状況がどのように変化するかを推測するため)、その履歴を保持するのはZen次第です。同様に、どのようなデータを保持し、どのように構造化するかを決めるのはZen次第です。Zenは、温度で必要のないものは何でも捨てることができます。単一の値はそれほど良い例ではありませんが、より豊富な情報のレコードを送信している場合は明らかに役立ちます。
この違いの良い例は、XML処理の兄弟であるSAXとDOMです。DOMはリクエストコラボレーションを使用します。ドキュメントを読み込むように指示し、情報を取得するためのリクエストを発行します。SAXでは、パーサーがデータソースを読み取るときに、さまざまなイベントをリッスンします。DOMはデータを保持しますが、SAXはステートレスです。保持する状態を選択する必要があります。2つは非常に異なるプログラミングモデルになります。要素のコンテキストに大きく依存する要素の処理を行う場合は、DOMの方がはるかに便利ですが、コンテキストが不要な場合はSAXの方が優れています。また、SAXはこのためオーバーヘッドがはるかに少なく、多くの場合、より迅速に動作します。
このもう1つの結果は、データがすぐに複製されることです。データのすべてのユーザーが独自のコピーを保持します。多くの設計者にとって、データの複製はひどい悪です。結局のところ、複数のデータソースがある場合、2つの時計を持っているが、時間がわからない人と同じです。しかし、これは非常に制御された複製です。時計は1つだけで、物事がいつ変化するかを検出するために常にエーテルを監視しています。もちろん、接続がダウンしている場合は何かを見逃しますが、リクエストシナリオでは、ソースへの接続なしにクエリを発行することはできません。イベントキューは通常、メッセージがオンラインに戻るまで待ってから配信されるようにします。
各データビットに1つのソースのみがあることを保証するために、データの変更を制御できます。ただし、1つのコンポーネントのみがデータを格納すると言うのではなく、1つのコンポーネントがそのデータビットに関する更新イベントを投稿できるようにすることでこれを行います。
すべての顧客データが中央の顧客管理アプリケーションによって管理されるシナリオを想像してみてください。特に、顧客の住所を管理します。また、シングルモルトのテイスティングスキルを実践的に向上させることが重要だと信じている人々に報酬を与える、常連客プログラムもあります。常連客のWebサイトを使用してLagavulinの特価ボトルを注文すると、Webサイトとのセッション中に顧客管理システムにクエリを送信して、私の配送先住所を取得します。このセッション中に住所を更新したい場合、常連客アプリケーションは私のリクエストを受け取り、顧客管理システムにコマンドメッセージを送信して更新を行います。
このシナリオをイベント駆動型の世界に移行すると、顧客データを必要とするすべてのアプリケーションは、必要な顧客データの独自の読み取り専用コピーを保持します。そのため、アイラネクターを注文する際に、常連客アプリケーションは顧客住所の独自のレプリカを参照するだけです(常連客プログラムに参加している人の住所のみを保存する必要があります)。住所の変更をリクエストした場合、シナリオはリクエスト駆動型の場合とほぼ同じです。常連客アプリは顧客管理アプリケーションにコマンドメッセージを送信します。唯一の違いは、顧客管理アプリケーションが住所が変更されたことを示すイベントを投稿し、それが常連客アプリケーションに住所のレコードを更新させることです。
この例は、データを集中管理したい場合は、2種類のイベントが必要になることを示しています。管理アプリケーション以外は無視する必要があるデータの更新リクエストと、全員が対応する確認済みの更新です。これは、更新リクエストに別のチャネルを使用することにもつながる可能性があります。
イベントカスケード
イベントコラボレーションを使用する場合、カスケードの結果に注意する必要があります。イベントカスケードは、一部の人にとっては湖の怪物と見なされ、他の人はそれを喜びます。
イベントカスケードとは、一連のイベントが他のイベントをトリガーする場合に発生する現象です。通常とは異なり、抽象的な例で説明します。A、B、Cの3つのイベントがあるとします。イベントAに取り組んでいるときは、Bが結果であること(A -> B)を理解できます。これは良いことです。他の誰かがBに取り組んでおり、Cが結果であること(B -> C)を理解できます。これも良いことです。しかし、今ではA -> B -> Cのカスケードが発生しています。これは、カスケード全体を把握するには両方のコンテキストを橋渡しする必要があるため、見づらい場合があります。Aについて考えているときはCについては考えず、Bに取り組んでいるときはAについては考えません。その結果、このカスケードは予期しない動作を引き起こす可能性があります。これは良いことかもしれませんが、問題になることもあります。このような3段階のカスケードは単純なケースですが、明らかにカスケードは任意に長くなる可能性があり、長くなるほど驚くべき結果になる可能性があります。
もう少し現実的な例を見てみましょう。病院の手術室の管理システムを考えてみましょう。手術室は希少で高価なリソースであるため、最大限に活用するために慎重に管理する必要があります。手術は事前に予約されます。数週間前(私の治癒した腕からピンを抜いたときのように)の場合もあれば、かなり迅速に(骨折した腕を数時間以内に手術したときのように)の場合もあります。
手術室は希少なリソースであるため、手術がキャンセルまたは延期された場合、他の手術のスケジュールを組めるように部屋を解放する必要があります。そのため、部屋のスケジュールシステムは、手術延期イベントをリッスンし、手術が予定されていた部屋を解放する必要があります。
患者が処置のために来院すると、病院は一連の術前評価を実施します。これらの評価のいずれかで手術が禁忌となる場合、少なくとも臨床医が確認するまでは手術を延期する必要があります。そのため、手術スケジュールシステムは、術前の禁忌をリッスンし、禁忌となる手術を延期する必要があります。
これらのイベントの因果関係はどちらも理にかなっていますが、一緒になると意図しない影響を与える可能性があります。誰かが来院して禁忌と判断された場合、手術は延期され、部屋は解放されます。問題は、臨床医が数分以内に術前評価を確認して手術を続行することを決定するかもしれないということです。しかし、その間に部屋は別の手術で予約されてしまいます。これは、簡単な手術であっても、手術の準備と絶食をしなければならない患者にとって非常に不便です。
イベントカスケードは、何かが起こり、一連の局所的な論理イベント接続の結果として間接的に何かが起こるため、良いものです。イベントカスケードは、何かが起こり、一連の局所的な論理イベント接続の結果として間接的に何かが起こるため、悪いものです。イベントカスケードは、このように説明されると非常に明白に見えますが、実際に目にするまでは見つけるのが非常に難しい場合があります。これは、システム自体からメタデータをクエリすることによってイベントチェーンのグラフを構築できる視覚化システムが非常に役立つ分野です。
使用方法
イベントコラボレーションの最大の強みは、コンポーネント間の結合が非常に緩やかであることです。もちろん、これは最大の弱点でもあります。
イベントコラボレーションを使用すると、既存のコンポーネントが新しいコンポーネントについて何も知らなくても、システムに新しいコンポーネントを簡単に追加できます。新しいコンポーネントがイベントをリッスンする限り、コラボレーションできます。
イベントコラボレーションは、各コンポーネントをシンプルに保つのに役立ちます。コンポーネントが世界について知る必要があるのは、リッスンするイベントだけです。興味深いことが起こるたびに、イベントを発行します。他の誰かがリッスンしているかどうかを気にする必要さえありません。これにより、開発者は一度に1つのコンポーネント(入力が非常に適切に制御されたコンポーネント)に集中できます。
イベントコラボレーションは、イベントソーシングにとって素晴らしい環境です。すべての通信でイベントコラボレーションを使用する場合、イベントソーシングされたアプリケーションが入力にゲートウェイを使用してイベント通信を模倣する必要がなくなります。
イベントコラボレーションを使用するシステムは、障害に対する耐障害性が高くなります。各コンポーネントは動作に必要なすべてのデータを持っているため、外部との通信が失われた場合でも動作を続行できます。しかし、これは諸刃の剣です。コンポーネントは動作を続行できますが、変化に合わせてイベントを受信していない場合は、古い情報に基づいて動作します。そのため、古い情報に基づいてアクションを開始する可能性があります。リクエストコラボレーションでは、単に機能しません。これは、状況によっては望ましい場合があります。
個々のコンポーネントは単純になりますが、相互作用の複雑さは増します。これらの相互作用はソースコードを読んでも明確ではないため、事態はさらに悪化します。リクエストコラボレーションでは、あるコンポーネントからの呼び出しが別のコンポーネントからの反応につながることが簡単にわかります。ポリモーフィズムを使用しても、物事を調べて結果を確認するのは難しくありません。イベントコラボレーションでは、実行時まで誰がイベントをリッスンしているかを知ることができません。これは実際には、コンポーネント間のリンクを構成データでのみ見つけることができることを意味します。また、構成データの領域が複数ある場合があります。その結果、これらの相互作用を見つけて理解し、デバッグするのは困難です。ここでは、実行時にコンポーネントの構成を表示できる自動化ツールを使用して、現状を確認するのが非常に役立ちます。
各参加者は必要なすべてのデータを保存するため、多くのデータが複製されます。システムは必要なデータのみを保存するため、サブセットのみを取得するため、これは思ったほど多くない可能性があります。イベント自体も、監査証跡として機能し、エラーリカバリなどに役立つように保存する必要があります。非常に大規模なデータセットでは、これは問題になる可能性があります。ただし、特にストレージコストは、最近では他のほとんどのものよりも急速に低下しています。
謝辞
同僚のイアン・カートライトは、このパターンに関する彼の経験で私を大いに助けてくれました。ダグ・マーシーは、イベントカスケードに関する現実的な例を提供してくれました。
例:取引(C#)
株式取引の例を使用して、イベントコラボレーションとそれがリクエストレスポンススタイルとどのように異なるかを示すことにしました。基本的な例から始めて、システムに対するいくつかの変更がコラボレーションスタイルによってどのように異なる動作をするかを強調します。
最初に使用する基本的な設定は、取引を行うトレーダーがいて、その取引が証券取引所に送信されるというものです。これらの取引は、取引所が執行されたことを通知するまで未確認です。これが簡単なテストです
[Test] public void NarrativeOfOrderLifeCycle() { traderA.PlaceOrder("TW", 1000); Assert.AreEqual(1, traderA.OutstandingOrders.Count); ExecuteAllOrders(); Assert.AreEqual(0, traderA.OutstandingOrders.Count); }
これが2つのスタイルでどのように機能するかを示すために、C#のインメモリオブジェクトとしての両方のスタイルのインタラクションを調べます。ネットワークを介して動作する複数のマシンでも同じ原則が適用されます。いずれの場合も、複数のトレーダーオブジェクトと複数の証券取引所オブジェクトがあります。
リクエスト/レスポンススタイルでこれがどのように機能するかから始めます。配置と実行の2つのインタラクションを考慮する必要があります。配置の場合、トレーダーオブジェクトは証券取引所に取引が行われたことを伝える必要があります。
クラス Trader...
public void PlaceOrder(string symbol, int volume) { StockExchange exchange = ServiceLocator.StockExchangeFor(symbol); Order order = new Order(symbol, volume, this); exchange.SubmitOrder(order); }
ここでは、各株式が単一の取引所で取引され、ルックアップを介して正しい取引所を見つけると想定しています。注文は非常にシンプルで、必要なのはシンボル、数量、そして注文を行ったトレーダーだけです。
証券取引所オブジェクトは、単に新しい注文を未処理注文のリストに追加することで反応します。
クラス StockExchange...
public void SubmitOrder(Order order) { outstandingOrders.Add(order); } private List<Order> outstandingOrders = new List<Order>();
証券取引所オブジェクトへの注文の送信は、コマンド操作の例です。取引所に何をしたいかを伝えます。何か問題が発生しない限り(その場合は例外が発生します)、結果については実際には気にしません。コマンドは同期でもあり、処理を続行する前に証券取引所からの応答を待ちます。その応答は、取引所が送信を受け取ったことだけを意味する場合があり、検証が行われたことを意味する場合があります。呼び出し元への参照(注文内)を提供しているため、取引所は後で注文についてトレーダーに連絡する場合があります。
次に、実行のコードを見てみましょう。
クラス StockExchange...
public void Execute(Order o) { outstandingOrders.Remove(o); }
取引所オブジェクトに関しては、実際の取引所が必要な処理を行ったという記録として、未処理注文のリストから注文を削除するだけです。トレーダーが未処理の注文を確認する必要がある場合は、取引所に問い合わせます。
クラス Trader...
public List<Order> OutstandingOrders { get { List<Order> result = new List<Order>(); ServiceLocator.ForEachExchange(delegate(StockExchange ex) { result.AddRange(ex.OutstandingOrdersFor(this)); }); return result; } }

図3:配置、実行、およびトレーダークエリ。のインタラクションをまとめたシーケンス図
次に、イベントコラボレーションを使用して同じシナリオを見てみましょう。配置から再び始めます。
クラス Trader...
public void PlaceOrder(string stock, int volume) { Order order = new Order(stock, volume); outstandingOrders.Add(order); MessageBus.PublishOrderPlacement(order); }
最初に注意すべき点は、今回はトレーダーが未処理の注文を記録していることです。これは、状態を維持する責任のシフトの一部です。トレーダーは状態を必要とするため、その状態を維持するのはトレーダーの責任です。
2つ目のシフトは、証券取引所への外部通信の性質です。ここでは、トレーダーは一般的なメッセージバスに通知します。メッセージバスは、関心のある人にイベントを渡すことができます。証券取引所がイベントを確認するには、その種類のイベントをサブスクライブする必要があります。これはコンストラクターで行います。
クラス StockExchange...
public StockExchange() { MessageBus.SubscribeToOrders(delegate(Order o) { orderReceivedCallback(o); }); }
クラス MessageBus...
public static void SubscribeToOrders(OrderPlacedDelegate method) { instance.OrderPlaced += method; } public event OrderPlacedDelegate OrderPlaced; public delegate void OrderPlacedDelegate(Order order);
ここでは、C#のイベントメカニズムを公開およびサブスクライブメソッド内にラップしています。これは厳密には必要ありませんが、C#以外の人々が理解しやすいと考えています。
トレーダーが配置イベントを公開すると、メッセージバスはその情報をサブスクライバーにプッシュします。
クラス MessageBus...
public static void PublishOrderPlacement(Order order) { if (instance.OrderPlaced != null) { instance.OrderPlaced(order); } }
クラス StockExchange...
private void orderReceivedCallback(Order order) { if (orderForThisExchange(order)) outstandingOrders.Add(order); }
次に、実行を見てみましょう。これは再び証券取引所オブジェクトから始まります。
クラス StockExchange...
public void Execute (Order o) { MessageBus.PublishExecution(o); outstandingOrders.Remove(o); }
ここでは、リクエスト/レスポンスの場合と同様に、未処理のリストから注文を削除します。ただし、イベントも公開します。これはトレーダーによって取得されます。
クラス Trader...
public Trader() { MessageBus.SubscribeToExecutions(delegate(Order o) { orderExecutedCallback(o); }); } private void orderExecutedCallback(Order o) { if (outstandingOrders.Contains(o)) outstandingOrders.Remove(o); }
(メッセージバスでの公開/サブスクライブの実装は省略しました。前の場合と同じです。)
トレーダーは、証券取引所からのイベントに応じて内部状態を更新します。そのため、未処理の注文を判断するには、他のオブジェクトにクエリを発行するのではなく、独自の内部状態を確認するだけです。
クラス Trader...
public IList<Order> OutstandingOrders { get{ return outstandingOrders.AsReadOnly();} }

図4:イベントを使用した配置、実行、およびトレーダークエリのインタラクションをまとめたシーケンス図。
リスク追跡コンポーネントの追加
これまでは、トレーダーと証券取引所がこれら2つのスタイルでどのように連携するかを見てきました。3番目のコンポーネントを追加する方法が2つのスタイルでどのように異なるかを確認することで、違いをさらに理解できます。
追加する3番目のコンポーネントは、リスク管理コンポーネントです。その仕事は、特定の株式を持つすべてのトレーダーの未処理の総量を確認することです。
クラス StockRrRiskTester...
[Test] public void ShouldTrackTotalOutstandingVolumeForOrders() { RiskTracker tracker = new RiskTracker(null); traderA.PlaceOrder("TW", 1000); traderA.PlaceOrder("ICMF", 7000); Trader traderB = new Trader(); traderB.PlaceOrder("TW", 500); Assert.AreEqual(1500, tracker.TotalExposure("TW")); }
さらに、リスク管理システムは、未処理の合計がプリセット制限を超えた場合にアラートメッセージを送信し、制限を下回った場合はアラートをキャンセルする必要があります。
クラス StockRrRiskTester...
[Test] public void ShouldTrackAlertWhenOverLimitForStock() { string symbol = "TW"; ServiceLocator.RiskTracker.SetLimit(symbol, 2000); traderA.PlaceOrder(symbol, 2001); Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent); }
これが機能するには、リスク管理システムが注文の配置(エクスポージャーの増加)と注文の実行(エクスポージャーの減少)の両方に関与する必要があります。
リクエスト/レスポンスシナリオから見てみましょう。配置から始めます。注文が送信されるたびにリスクマネージャーに警告するように、証券取引所オブジェクトの送信メソッドを変更することを選択しました。
クラス StockExchange...
public void SubmitOrder(Order order) { outstandingOrders.Add(order); ServiceLocator.RiskTracker.CheckForAlertsOnOrderPlacement(order.Stock); }
この変更はトレーダーで行うこともできますが、取引所を選択しました。これは、実行のためにとにかくこの依存関係が必要になるため、トレーダーからリスクトラッカーへの依存関係を作成することを回避できるためです。
リスクトラッカーが総エクスポージャーを判断するには、関連する証券取引所に対してクエリを発行します。
クラス RiskTracker...
public int TotalExposure(string symbol) { return ServiceLocator.StockExchangeFor(symbol).OutstandingVolumeForStock(symbol); }
そのため、新しい配置について通知されると、リスクトラッカーはこのクエリの結果を保持している制限と比較してテストします。
クラス RiskTracker...
public void CheckForAlertsOnOrderPlacement(String symbol) { if (OverLimit(symbol)) gateway.GenerateAlert(symbol); alertedStocks.Add(symbol); } private bool OverLimit(String symbol) { return stockLimit.ContainsKey(symbol) && TotalExposure(symbol) > stockLimit[symbol]; }
取引が執行されると、再びリスクトラッカーが証券取引所によって呼び出される必要があります。トラッカーは再び現在の総エクスポージャーを確認し、アラートをキャンセルする必要があるかどうかを判断します。
クラス StockExchange...
public void Execute(Order o) { outstandingOrders.Remove(o); ServiceLocator.RiskTracker.CheckForCancelledAlerts(o.Stock); }
クラス RiskTracker...
public void CheckForCancelledAlerts(String symbol) { if (alertedStocks.Contains(symbol) && !OverLimit(symbol)) gateway.CancelAlert(symbol); }

図5:リクエスト/レスポンスを使用して、リスク制限を超える注文の配置がアラートをトリガーする方法を示すシーケンス図。
では、イベントコラボレーションを使用する場合を見てみましょう。重要な違いは、リスクトラッカーを追加しても、既存のコンポーネントに変更を加える必要がないことです。代わりに、既存のコンポーネントが公開するイベントをリスニングするようにリスクトラッカーを構築します。
クラス RiskTracker...
public RiskTracker() { stockPosition = new Dictionary<string, int>(); stockLimit = new Dictionary<string, int>(); MessageBus.SubscribeToOrders(delegate(Order o) { handleOrderPlaced(o); }); MessageBus.SubscribeToExecutions(delegate(Order o) { handleExecution(o); }); }
トラッカーは、ポジションを追跡するためのデータ構造も必要とします。エクスポージャーを判断するために証券取引所にクエリを実行することはないためです。
取引が行われると、トラッカーはイベントをピックアップします。それに応答して、ポジションのコピーを更新し、制限を超えているかどうかを確認します。
クラス RiskTracker...
private void handleOrderPlaced(Order order) { if (!stockPosition.ContainsKey(order.Stock)) stockPosition[order.Stock] = 0; stockPosition[order.Stock] += order.Volume; checkForOverLimit(order); } private void checkForOverLimit(Order order) { if (OverLimit(order)) MessageBus.PublishAlertPosted(order); } private bool OverLimit(Order order) { return stockLimit.ContainsKey(order.Stock) && stockPosition[order.Stock] > stockLimit[order.Stock]; }
イベントコラボレーションを継続して使用するために、アラートが表示されたときにイベントを発生させます。それに応答してメールを送信したい場合は、そのイベントをリスニングするメールゲートウェイを作成できます。
実行イベントでは、ポジションのコピーを再度更新し、キャンセルを発生させる必要があるかどうかを確認します。
クラス RiskTracker...
private void handleExecution(Order o) { bool wasOverLimit = OverLimit(o); stockPosition[o.Stock] -= o.Volume; if (wasOverLimit && !OverLimit(o)) MessageBus.PublishAlertCancelled(o); }

図6:イベントを使用して注文をするとアラートがトリガーされます。
この変更により、2つのインタラクションスタイルの重要な違いが明らかになります。
- リクエスト-レスポンスコラボレーションでは既存のコンポーネントを変更する必要がありましたが、イベントコラボレーションでは変更する必要がありませんでした。これは、既存のコンポーネントがすべての関連する変更情報をブロードキャストするためです。
- イベントコラボレーションでは、新しいコンポーネントに依存関係を追加しませんでした。
- イベントコラボレーションリスクトラッカーは、必要な状態をすでに含んでいるため、他のコンポーネントとさらに通信することなくアラートを判断できました。これにより、コンポーネント間の呼び出しの量が削減されました。これは、コンポーネントが別々のプロセスにある場合に重要になる可能性があります。 リクエスト-レスポンスコラボレーションで、呼び出しの一部として現在の総エクスポージャーを渡すことで同じ効果を実現できます。ただし、これは、証券取引所オブジェクトがリスクトラッカーが作業を行うために必要なデータを知る必要があることを意味し、2つのコンポーネント間の結合がさらに緊密になります。
- リスクトラッカーを追加する際に、イベントを使用して新しく追加された情報をブロードキャストし、全体的なコラボレーションにおいて優れた役割を果たせるようにしました。(そして実際に、それを使用してメールゲートウェイをトリガーしました。)リスクトラッカーによって公開されたイベントは、トラッカーの状態の変更に関連付けられていませんでした(ポジションの変更をブロードキャストしなかったため)。しかし、トラッカーがグローバルな知識に追加した新しい情報、つまりエクスポージャーが制限を超えているかどうか、に関連付けられています。
多くの人は、これらの違いは、リクエスト-レスポンスコラボレーションよりもイベントコラボレーションの方が結合が弱いシステムに関する根本的な違いであると考えるでしょう。私たちはそれほど確信していません。リクエスト-レスポンスコラボレーションでは、コンポーネントは、可能なリクエストのメニューとして表現されるインターフェースを介して相互に結合されます。しかし、イベントコラボレーションにも同じ基本的な結合が存在します。インターフェースがリクエスト呼び出しからイベントに変更されただけです。コンポーネントのイベントを変更すると、他のコンポーネントに影響が及ぶ可能性があります。
重要な違いは、コンポーネント間の通信フローがコンポーネント内に含まれなくなったことです。イベントコラボレーションを使用すると、証券取引所にリスクトラッカーにアラートを確認するタイミングを指示する必要がなくなります。この動作は、イブニングモデルによって暗黙的に発生します。
しかし、この暗黙的な動作には、負の側面があります。コンポーネントに別の変更を加えることを想像してみましょう。これまでのところ、注文は完全に実行されると想定していました。部分的な実行を処理するように証券取引所を変更してみましょう。
[Test] public void ShouldCancelAlertIfParitalExecutionTakesBelowLimit() { StockExchange exchange = ServiceLocator.StockExchangeFor("TW"); ServiceLocator.RiskTracker.SetLimit("TW", 2000); traderA.PlaceOrder("TW", 3000); Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent); Order theOrder = exchange.OutstandingOrders[0]; exchange.Execute(theOrder, 1000); Assert.AreEqual(1, ServiceLocator.AlertGateway.CancelsSent); } [Test] public void ShouldNotCancelAlertIfParitalExecutionStillAboveLimit() { StockExchange exchange = ServiceLocator.StockExchangeFor("TW"); ServiceLocator.RiskTracker.SetLimit("TW", 2000); traderA.PlaceOrder("TW", 3000); Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent); Order theOrder = exchange.OutstandingOrders[0]; exchange.Execute(theOrder, 999); Assert.AreEqual(0, ServiceLocator.AlertGateway.CancelsSent); }
これを行うために必要な変更を見てみましょう。リクエスト-レスポンスコラボレーションの場合、部分的な実行を記録するように証券取引所を変更する必要があります。
クラス StockExchange...
public void Execute(Order o, int volume) { o.ExecutedAmount += volume; if (o.IsFullyExecuted) outstandingOrders.Remove(o); ServiceLocator.RiskTracker.CheckForCancelledAlerts(o.Stock); } public void Execute(Order o) { Execute(o, o.Volume); }
class Order...
public int ExecutedAmount { get { return executedVolume; } set { executedVolume = value; } } public bool IsFullyExecuted { get { return executedVolume == volume; } } private int executedVolume;
リスクトラッカーへの参照がそこにあるため、部分的な実行ケースを考慮してリスクトラッカーを変更することを思い出させてくれます。ただし、トラッカーは証券取引所にクエリを実行することで未処理の合計を取得するため、実際にはリスクトラッカーをまったく変更する必要はありません。
イベントコラボレーションの場合を見ると、事態は少し複雑になります。証券取引所オブジェクトの基本的な変更は似ています。
クラス StockExchange...
public void Execute(Order o, int amount) { o.ExecutedAmount += amount; MessageBus.PublishExecution(o); if (o.IsFullyExecuted) outstandingOrders.Remove(o); } public void Execute(Order o) { Execute(o, o.Volume); }
ただし、リスクトラッカーへのリンクの表示がないため、変更を検討する必要があることを示唆するものはありません。実行された金額は注文のプロパティであるため、静的に型付けされたシステムであっても、すべてが正常にコンパイルされます。しかし、リスクトラッカーは、データの変更を適切にキャプチャしていないため、より明白な障害の兆候なしに、誤った情報を出し始めます。
これはもちろん、暗黙的な動作を持つシステムの固有の弱点です。見えないため、変更時にどのような変更を加える必要があるかがわかりません。ロジックの流れがコードで明示されていないため、デバッグも難しい場合があります。
これは、イベントコラボレーションを使用すると、一部の変更は容易になりますが、他の変更は難しくなることを示しています。2つの異なるスタイルの真の理解に基づいた経験を実際に捉えているものはあまり見ていないため、トレードオフの公平な全体像を把握するのは容易ではありません。