イベントソーシング
アプリケーションの状態に対するすべての変更を、イベントのシーケンスとしてキャプチャします。
2005年12月12日
これは、2000年代半ばに私が取り組んでいたエンタープライズアプリケーションアーキテクチャの開発に関する執筆の一部です。残念ながら、他の多くのことに気を取られてしまい、それ以上取り組む時間がありませんでした。また、近い将来も時間を見つけることができそうにありません。そのため、この資料はまだ下書きの状態であり、再び取り組む時間ができるまでは修正や更新を行う予定はありません。
アプリケーションの状態を照会して世界の現在の状態を知ることができ、これにより多くの疑問に答えることができます。しかし、現在地を知りたいだけでなく、どのようにそこにたどり着いたのかを知りたい場合もあります。
イベントソーシングは、アプリケーションの状態に対するすべての変更がイベントのシーケンスとして保存されることを保証します。これらのイベントを照会できるだけでなく、イベントログを使用して過去の状態を再構築したり、遡及的な変更に対応するために状態を自動的に調整するための基盤として使用したりすることもできます。
仕組み
イベントソーシングの基本的な考え方は、アプリケーションの状態に対するすべての変更がイベントオブジェクトにキャプチャされ、これらのイベントオブジェクトがアプリケーションの状態自体と同じライフタイムで適用された順序で保存されることを保証することです。
船舶の通知に関する簡単な例を考えてみましょう。この例では、外洋に多くの船舶があり、それらの位置を知る必要があります。これを行う簡単な方法は、船舶が港に到着または出発したときに通知するためのメソッドを備えた追跡アプリケーションを用意することです。

図1:船舶の移動を追跡するためのシンプルなインターフェース。
この場合、サービスが呼び出されると、関連する船舶を見つけてその場所を更新します。船舶オブジェクトは、船舶の現在判明している状態を記録します。
イベントソーシングを導入すると、このプロセスにステップが追加されます。現在、サービスは変更を記録するためのイベントオブジェクトを作成し、それを処理して船舶を更新します。

図2:変更をキャプチャするためのイベントの使用。
処理だけを見ると、これは不必要な間接レベルです。興味深い違いは、いくつかの変更後にアプリケーションに永続化されるものを見たときです。いくつかの簡単な変更を想像してみましょう。
- 船「キングロイ」がサンフランシスコを出港
- 船「プリンストレバー」がロサンゼルスに到着
- 船「キングロイ」が香港に到着
基本的なサービスでは、船舶オブジェクトによってキャプチャされた最終状態のみが表示されます。これをアプリケーションの状態と呼びます。

図3:シンプルなトラッカーによって追跡されたいくつかの移動後の状態。
イベントソーシングでは、各イベントもキャプチャします。永続的なストアを使用している場合、イベントは船舶オブジェクトと同じように永続化されます。アプリケーションの状態とイベントログという2つの異なるものを永続化していると言うと役に立ちます。

図4:イベントソースされたトラッカーによって追跡されたいくつかの移動後の状態。
イベントソーシングを使用することで得られた最も明白なことは、すべての変更のログができたことです。各船がどこにあるかだけでなく、どこにいたかも確認できます。しかし、これは小さな利点です。船舶オブジェクトに過去の港の履歴を保持したり、船舶が移動するたびにログファイルに書き込むことでも、これを行うことができます。これらの両方で適切な履歴を提供できます。
イベントソーシングの鍵は、ドメインオブジェクトに対するすべての変更がイベントオブジェクトによって開始されることを保証することです。これにより、イベントログの上に構築できる多くの機能につながります。
- 完全な再構築:アプリケーションの状態を完全に破棄し、空のアプリケーションでイベントログからイベントを再実行することで再構築できます。
- 時間的クエリ:任意の時点でのアプリケーションの状態を判断できます。概念的には、空白の状態から開始し、特定の時間またはイベントまでのイベントを再実行することでこれを行います。複数のタイムライン(バージョン管理システムのブランチに類似)を考慮することで、これをさらに進めることができます。
- イベントリプレイ:過去のイベントが誤っていたことが判明した場合、それを反転し、後のイベントを反転してから、新しいイベントと後のイベントを再生することで、結果を計算できます。(または、アプリケーションの状態を破棄し、正しいイベントをシーケンスに入れてすべてのイベントを再生することで。)同じ手法で、間違ったシーケンスで受信したイベントを処理できます。これは、非同期メッセージングで通信するシステムでよくある問題です。
イベントソーシングを使用するアプリケーションの一般的な例は、バージョン管理システムです。このようなシステムは、時間的クエリを頻繁に使用します。Subversionは、ダンプとリストアを使用してリポジトリファイル間でデータを移動するときはいつでも、完全な再構築を使用します。情報に特に関心がないため、イベントリプレイを行うものは認識していません。 イベントソーシングを使用するエンタープライズアプリケーションはまれですが、それを使用するいくつかのアプリケーション(またはアプリケーションの一部)を見たことがあります。
アプリケーション状態のストレージ
イベントソーシングを使用する最も簡単な考え方は、空白のアプリケーション状態から開始し、イベントを適用して目的の状態に到達することで、要求されたアプリケーションの状態を計算することです。これが遅いプロセスである理由を理解することも同様に簡単です。特にイベントが多い場合はなおさらです。
多くのアプリケーションでは、最近のアプリケーション状態を要求することが一般的です。その場合、より高速な代替手段は現在のアプリケーション状態を保存することです。誰かがイベントソーシングが提供する特別な機能を必要とする場合は、その追加機能が上に構築されます。
アプリケーションの状態は、メモリまたはディスクに保存できます。アプリケーションの状態はイベントログから純粋に派生できるため、好きな場所にキャッシュできます。稼働日に使用中のシステムは、夜間のスナップショットから一日の初めに起動し、現在のアプリケーション状態をメモリに保持できます。クラッシュした場合は、夜間のストアからイベントを再生します。稼働日の終わりに、状態の新しいスナップショットを作成できます。実行中のアプリケーションを停止することなく、いつでも新しいスナップショットを並行して作成できます。
公式なシステム記録は、イベントログまたは現在のアプリケーション状態のいずれかです。現在のアプリケーションの状態がデータベースに保持されている場合、イベントログは監査および特別な処理のためだけに存在する可能性があります。あるいは、イベントログを公式記録とし、必要なときにいつでもデータベースをそれらから構築できます。
イベントハンドラロジックの構造化
イベントを処理するロジックをどこに配置するかについて、いくつかの選択肢があります。主な選択肢は、ロジックをトランザクションスクリプトに配置するか、ドメインモデルに配置するかです。通常どおり、トランザクションスクリプトはシンプルなロジックに適しており、ドメインモデルは物事がより複雑になった場合に適しています。
一般に、イベントまたはコマンドを通じて変更を推進するアプリケーションでトランザクションスクリプトを使用する傾向があることに気づきました。実際、一部の人は、これがこのように駆動されるシステムを構造化するための必要な方法であると信じています。しかし、これは幻想です。
これを考える良い方法は、2つの責任が関与しているということです。ドメインロジックの処理は、アプリケーションを操作するビジネスロジックです。選択ロジックの処理は、受信イベントに応じて実行する必要がある処理ドメインロジックのチャンクを選択するロジックです。これらを組み合わせることができます。基本的にこれはトランザクションスクリプトアプローチですが、イベント処理システムに処理選択ロジックを配置し、処理ドメインロジックを含むドメインモデル内のメソッドを呼び出すことで分離することもできます。
この決定を下したら、次は処理選択ロジックをイベントオブジェクト自体に配置するか、別のイベントプロセッサオブジェクトを使用するかです。プロセッサの問題は、イベントのタイプに応じて異なるロジックを実行する必要があることであり、これは優れたOOerにとって忌み嫌われるタイプのタイプスイッチです。すべてが同じであれば、イベントの種類によって変化するため、イベント自体に処理選択ロジックを配置する必要があります。
もちろん、すべてが常に同じというわけではありません。別のプロセッサを持つことが理にかなっている場合の1つは、イベントオブジェクトがDTOであり、イベントにコードを配置することを禁止する何らかの自動手段によってシリアル化およびデシリアル化される場合です。この場合、イベントの選択ロジックを見つける必要があります。できればこれを避けるのが私の考えですが、避けられない場合は、DTOをイベントの隠れたデータホルダーとして扱い、イベントを通常のポリモーフィックオブジェクトとして扱います。この場合、構成ファイルまたは(より良い)命名規則を使用して、シリアル化されたイベントDTOを実際のイベントに一致させるために、適度に巧妙なことを行う価値があります。
イベントを反転する必要がない場合、ドメインモデルをイベントログに無知にすることは簡単です。ロジックを反転すると、ドメインモデルが前の状態を保存および取得する必要があるため、これがよりトリッキーになり、ドメインモデルがイベントログを認識する方がはるかに便利になります。
イベントの反転
イベントが前進するだけでなく、それらを反転させることができると便利な場合もあります。
リバーサル(逆転)は、イベントが差分の形で表現されている場合に最も簡単になります。例えば、「マーティンの口座に10ドルを追加する」は、「マーティンの口座を110ドルに設定する」よりも簡単です。前者の場合、10ドルを差し引くだけで逆転できますが、後者の場合、口座の過去の値を再現するための十分な情報がありません。
入力イベントが差分アプローチに従わない場合は、イベントは処理中にリバーサルに必要なすべてのものを確実に保存する必要があります。変更された値の以前の値を保存するか、イベントで差分を計算して保存することでこれを行うことができます。
この保存の要件は、処理ロジックがドメインモデル内にある場合に重要な影響を与えます。ドメインモデルは、イベントオブジェクトの処理に可視であるべきではない方法で内部状態を変更する可能性があるからです。この場合、ドメインモデルはイベントを認識し、それらを使用して以前の値を保存できるように設計するのが最善です。
イベントの逆転のすべての機能は、過去のスナップショットに戻ってイベントストリームを再生することでも代わりに実行できることを覚えておくことが重要です。結果として、機能のために逆転が絶対に必要になることはありません。しかし、多くの場合、少数のイベントを逆転する方が、多数のイベントを順方向に再生するよりもはるかに効率的である可能性があるため、効率に大きな違いが生じる可能性があります。
外部更新
イベントソーシングの難しい要素の1つは、このアプローチに従わない外部システム(そしてほとんどのシステムがそうではありません)をどのように処理するかということです。外部システムにモディファイアメッセージを送信する場合や、他のシステムからクエリを受信する場合に問題が発生します。
イベントソーシングの利点の多くは、イベントを自由に再生できる機能に由来しますが、これらのイベントが外部システムに更新メッセージを送信する場合、これらの外部システムは実際の処理と再生の違いを認識していないため、問題が発生します。
これを処理するには、外部システムをゲートウェイでラップする必要があります。これは、いずれにせよ非常に良い考えであるため、それほど厄介ではありません。ゲートウェイは、イベントソーシングシステムが実行している再生処理を処理できるように、もう少し高度である必要があります。
再構築と時間的なクエリの場合、通常、再生処理中にゲートウェイを無効にできることが十分です。ドメインロジックに不可視な方法でこれを行う必要があります。ドメインロジックがPaymentGateway.sendを呼び出す場合、再生モードであるかどうかに関係なく呼び出す必要があります。ゲートウェイは、イベントプロセッサへの参照を持ち、外部呼び出しを外部に渡す前に再生モードであるかどうかを確認することで、その区別を処理する必要があります。
遡及的イベントを使用している場合、外部更新はさらに複雑になります。詳細については、そちらのディスカッションを参照してください。
外部システムで見られる可能性のあるもう1つの戦術は、時間で外部通知をバッファリングすることです。外部通知をすぐに作成する必要はなく、代わりに月末にのみ作成する必要がある場合があります。この場合、その時間が現れるまで、より自由に再処理できます。これは、リリース日まで外部メッセージを保存するゲートウェイを持つか、外部メッセージをすぐに通知するのではなく、通知ドメインイベントを介してトリガーすることで対処できます。
外部クエリ
外部クエリの主な問題は、返されるデータがイベントの処理結果に影響を与えることです。12月5日の為替レートを要求し、12月20日にそのイベントを再生すると、後のレートではなく12月5日の為替レートが必要になります。
外部システムが日付を指定して値を要求することで過去のデータを提供できる場合があります。それができ、信頼できると信頼できる場合は、それを使用して一貫した再生を保証できます。また、イベントコラボレーションを使用している場合もあります。その場合は、変更の履歴を保持することを確認するだけです。
これらの単純な計画を使用できない場合は、もう少し複雑なことを行う必要があります。1つのアプローチは、外部システムへのゲートウェイを設計して、クエリへの応答を記憶し、再生中にそれらを使用することです。これを完全に行うには、すべての外部クエリへの応答を記憶する必要があります。外部データがゆっくりと変化する場合は、値が変化した場合にのみ変更を記憶するのが妥当かもしれません。
外部インタラクション
外部システムへのクエリと更新の両方が、イベントソーシングで多くの複雑さを引き起こします。両方を含むインタラクションでは、両方の最悪の事態が発生します。そのようなインタラクションは、結果(クエリ)を返すだけでなく、外部システムのステート変更も引き起こす外部呼び出しである可能性があります。たとえば、注文の配送情報を返す注文の送信などです。
コード変更
したがって、この議論では、イベントを処理するアプリケーションが同じままであるという仮定を立てました。明らかにそうではないでしょう。イベントはデータの変更を処理しますが、コードの変更はどうでしょうか?
ここでは、新しい機能、バグ修正、時間ロジックの3つの幅広い種類のコード変更を考えることができます。
新しい機能は、基本的にシステムに新しい機能を追加しますが、以前に発生したことを無効にするわけではありません。これらはいつでも自由に追加できます。古いイベントで新しい機能を利用したい場合は、イベントを再処理するだけで新しい結果が表示されます。
新しい機能を使用して再処理する場合、通常は外部ゲートウェイをオフにすることをお勧めします。これは通常の場合です。例外は、新しい機能にこれらのゲートウェイが含まれる場合です。それでも、過去のイベントについて通知したくない場合があります。通知する場合は、古いイベントの最初の再処理のために特別な処理を行う必要があります。それは面倒ですが、一度だけ行う必要があります。
バグ修正は、過去の処理を調べて、それが間違っていたことに気付いた場合に発生します。内部的なことについては、これは本当に簡単に修正できます。必要なのは修正を行い、イベントを再処理することだけです。これで、アプリケーションの状態は本来あるべき状態に修正されます。多くの状況で、これは本当に素晴らしいことです。
繰り返しますが、外部ゲートウェイは複雑さをもたらします。基本的に、ゲートウェイはバグありで発生したことと、バグなしで発生したことの違いを追跡する必要があります。考え方は、遡及的イベントで発生する必要があることと似ています。実際、検討すべき再処理が多い場合は、遡及的イベントメカニズムを実際に使用してイベント自体を置き換える価値があります。ただし、それを行うには、イベントがバグのあるイベントと正しいイベントの両方を正しく反転できることを確認する必要があります。
3番目のケースは、ロジック自体が時間の経過とともに変化する場合です。たとえば、「11月18日までは10ドル、その後は15ドルを請求する」のようなルールです。この種のもの(時制的なルール)は、実際にはドメインモデル自体に入る必要があります。ドメインモデルは、イベント処理の正しいルールを使用して、いつでもイベントを実行できる必要があります。これは条件付きロジックで実行できますが、時間ロジックが多すぎると、これは乱雑になります。より良い方法は、テンポラルプロパティにストラテジーオブジェクトをフックすることです。たとえば、chargingRules.get(aDate).process(anEvent)
のようなものです。この種のスタイルについては、契約ディスパッチャをご覧ください。
古いイベントをバグのあるコードを使用して処理する必要がある場合、バグの処理と時間ロジックの間に潜在的な重複があります。これにより、二時制的な動作につながる可能性があります。「10月1日にあった8月1日のルールに従ってこのイベントを反転し、現在ある8月1日のルールに従って置き換える」。明らかに、これは非常に混乱する可能性があります。本当に必要な場合を除き、この道を進まないでください。
これらの問題の一部は、コードをデータに入れることで処理できます。オブジェクトの構成を使用して処理を把握する適応型オブジェクトモデルを使用するのが1つの方法です。別の方法は、コンパイルを必要としない直接実行可能な言語を使用して、スクリプトをデータに埋め込むことです。たとえば、JavaアプリにJRubyを埋め込むなどです。もちろん、ここでの危険は、適切な構成管理を維持することです。処理スクリプトへの変更は、他の更新と同じ方法で処理されるようにすることで、これを行う傾向があります。 (ただし、現時点では、観察から推測に確実に移行しています。)
イベントとアカウント
会計システムのコンテキストで、イベントソーシング(および結果として生じるパターン)の特に強力な例をいくつか見てきました。両方とも、要件(監査は会計システムにとって非常に重要です)と実装の両方において、非常に優れた相乗効果があります。ここでの重要な要素は、ドメインイベントのすべての会計上の結果が会計エントリの作成であり、これらのエントリを元のイベントにリンクするように物事を手配できることです。これにより、変更の追跡、反転などに非常に優れた基盤が得られます。特に、さまざまな調整手法が簡素化されました。
実際、アカウントについて考える1つの方法は、会計エントリがアカウントの値を変更するすべてのイベントのログであるということです。つまり、アカウント自体がイベントソーシングの例です。
いつ使用するか
アプリケーションへのすべての変更をイベントとしてパッケージ化することは、誰もが快適に感じるインターフェーススタイルではなく、多くの人が扱いにくいと感じています。その結果、それは自然な選択ではなく、それを使用するということは、何らかの形式の戻りを得ることが期待されることを意味します。
1つの明らかなリターンは、イベントをシリアル化して監査ログを作成するのが簡単なことです。そのような監査証跡は監査に役立ちますが、ショックはありませんが、他の用途もあります。オンラインアカウントを扱いにくい状態にし、電話で助けを求めた人に話を聞きました。彼は、ヘルパーが彼が何をしたかを正確に伝え、それによってそれを修正する方法を理解できたことに感銘を受けました。このような機能を提供するには、ユーザーのインタラクションをウォークスルーできるように、監査証跡をサポートグループに公開する必要があります。イベントソーシングはこれを行うのに適した方法ですが、より通常のロギングメカニズムを使用してこれを行うこともできます。これにより、奇妙なインターフェースに対処する必要がなくなります。
この種の完全な監査ログのもう1つの用途は、デバッグに役立てることです。もちろん、本番環境の問題をデバッグするためにログファイルを使用するのは古い方法です。イベントソーシングはさらに進んで、テスト環境を作成し、イベントをテスト環境に再生して、デバッガーでテストを実行するときと同じように、停止、巻き戻し、再生する機能を備え、正確に何が起こったかを確認できます。これは、アップグレードを本番環境に投入する前に並行テストを行うのに特に役立ちます。テストシステムで実際のイベントを再生し、期待どおりの答えが得られることをテストできます。
イベントソーシングは、並列モデルまたは遡及的イベントの基礎です。これらのパターンのいずれかを使用する場合は、最初にイベントソーシングを使用する必要があります。実際、これは、イベントソーシングを使用して構築されなかったシステムにこれらのパターンを後付けすることが非常に難しい程度まで当てはまります。したがって、システムが後でこれらのパターンを必要とする可能性が妥当であると思われる場合は、今すぐイベントソーシングを構築することをお勧めします。これは、この決定を後のリファクタリングに任せるのは賢明ではない場合の1つであるようです。
イベントソーシングは、特にスケーラビリティを重視する場合、アーキテクチャ全体にいくつかの可能性をもたらします。最近では「イベント駆動型アーキテクチャ」にかなりの関心が集まっています。この用語は幅広い概念をカバーしていますが、ほとんどはイベントメッセージを介して通信するシステムを中心に展開しています。このようなシステムは、非常に疎結合な並列スタイルで動作でき、優れた水平スケーラビリティとシステム障害に対する回復力を提供します。
この例としては、多数のリーダーと少数のライターを持つシステムが挙げられます。イベントソーシングを使用すると、これはインメモリデータベースを持つシステムのクラスターとして提供でき、イベントストリームを通じて互いに最新の状態に保たれます。更新が必要な場合は、単一のマスターシステム(または単一のデータベースまたはメッセージキューを中心としたより緊密なサーバークラスター)にルーティングできます。このマスターシステムは、システムオブレコードに更新を適用し、結果として生じるイベントを広範なリーダーのクラスターにブロードキャストします。システムオブレコードがデータベース内のアプリケーション状態である場合でも、これは非常に魅力的な構造になる可能性があります。システムオブレコードがイベントログである場合、イベントログはロックを最小限に抑えるだけで済む純粋な付加的な構造であるため、非常に高いパフォーマンスを実現するためのオプションが豊富にあります。
もちろん、このようなアーキテクチャには欠点がないわけではありません。リーダーシステムは、イベント伝播のタイミングの違いにより、マスター(および互いに)と同期がずれる可能性があります。ただし、この広範なスタイルのアーキテクチャは使用されており、私はそれについてほぼ全面的に好意的なコメントを聞いています。
このようにイベントストリームを使用すると、イベントストリームを利用して独自のモデルを構築することで、新しいアプリケーションを簡単に追加することもできます。このモデルは、すべてのシステムで同じである必要はありません。これは、統合へのメッセージングアプローチとうまく適合するアプローチです。
例:船の追跡(C#)
ここでは、基本的なアイデアを伝えるための非常にシンプルなイベントソーシングの例を示します。この例では、開始点として機能するように意図的に非常にシンプルにしています。その後、より複雑な問題をいくつか探求するために、さらに例を使用します。
ドメインモデルは、貨物を輸送し、港間を移動する船のシンプルなモデルです。

モデルに影響を与えるイベントには、4種類あります。
- 到着:船が港に到着する
- 出発:船が港を出発する
- 積み込み:貨物が船に積み込まれる
- 積み下ろし:貨物が船から積み下ろされる
船を移動させる簡単な例を見てみましょう。
class Tester...
Ship kr; Port sfo, la, yyv; Cargo refact; EventProcessor eProc; [SetUp] public void SetUp() { eProc = new EventProcessor(); refact = new Cargo ("Refactoring"); kr = new Ship("King Roy"); sfo = new Port("San Francisco", Country.US); la = new Port("Los Angeles", Country.US); yyv = new Port("Vancouver", Country.CANADA) ; }
[Test] public void ArrivalSetsShipsLocation() { ArrivalEvent ev = new ArrivalEvent(new DateTime(2005,11,1), sfo, kr); eProc.Process(ev); Assert.AreEqual(sfo, kr.Port); } [Test] public void DeparturePutsShipOutToSea() { eProc.Process(new ArrivalEvent(new DateTime(2005,10,1), la, kr)); eProc.Process(new ArrivalEvent(new DateTime(2005,11,1), sfo, kr)); eProc.Process(new DepartureEvent(new DateTime(2005,11,1), sfo, kr)); Assert.AreEqual(Port.AT_SEA, kr.Port); }
これらのテストを機能させるには、到着と出発のイベントだけが必要です。イベントプロセッサは非常にシンプルです。
class EventProcessor...
IList log = new ArrayList(); public void Process(DomainEvent e) { e.Process(); log.Add(e); }
各イベントには、processメソッドがあります。
class DomainEvent...
DateTime _recorded, _occurred; internal DomainEvent (DateTime occurred) { this._occurred = occurred; this._recorded = DateTime.Now; } abstract internal void Process();
到着イベントは単にデータをキャプチャし、適切なドメインオブジェクトにイベントを転送するだけのprocessメソッドを持っています。
class DepartureEvent...
Port _port; Ship _ship; internal Port Port {get { return _port; }} internal Ship Ship {get { return _ship; }} internal DepartureEvent(DateTime time, Port port, Ship ship) : base (time) { this._port = port; this._ship = ship; } internal override void Process() { Ship.HandleDeparture(this); }
したがって、ここではイベントが処理選択ロジックを実行します。処理ドメインロジックは、船によって実行されます。
class Ship...
public Port Port; public void HandleDeparture(DepartureEvent ev) { Port = Port.AT_SEA; }
出発イベントは、単にShipの港を特殊なケースに設定します。私はイベントをドメインオブジェクトに渡していることに気づくでしょう。ここには、イベントがドメインオブジェクトが処理に必要なデータだけを渡すのか、イベント自体を渡すのかという選択があります。イベントを渡すことで、イベントはドメインロジックが必要とするデータを正確に知る必要がなくなります。イベントが後で追加のデータを取得した場合、署名を更新する必要はありません。イベントを渡すことの欠点は、ドメインロジックがイベント自体を認識することです。
そのシンプルなテストは、基本的なイベント処理がどのように機能するかを示しているだけです。次に、ドメインロジックがどのように機能するかを示す小さな例を示します。私たちの船は、私の本を世界中に移動させるための輸送ライン全体を持つという私のファンタジーを満たすために本を輸送しています。ご存じのとおり、カナダを経由して本を送ることは非常に危険です。それは、本が大量の「ええ」で汚染されるリスクがあるためです。ほとんどすべての文にええがある本を見たことがあります(長い文では2、3個になることがあります)。
したがって、私の完璧な書籍輸送システムは、貨物がカナダを通過したかどうかを検出できる必要があります。
class Tester...
[Test] public void VisitingCanadaMarksCargo() { eProc.Process(new LoadEvent(new DateTime(2005,11,1), refact, kr)); eProc.Process(new ArrivalEvent(new DateTime(2005,11,2), yyv, kr)); eProc.Process(new DepartureEvent(new DateTime(2005,11,3), yyv, kr )); eProc.Process(new ArrivalEvent(new DateTime(2005,11,4), sfo, kr)); eProc.Process(new UnloadEvent(new DateTime(2005,11,5), refact, kr)); Assert.IsTrue(refact.HasBeenInCanada); }
貨物は船間を移動したり、降ろしたりする可能性があるため、これらの北部の危険にさらされたかどうかを知る責任は貨物にあります。幸いなことに、リスクは実際に港にいる場合にのみ発生し、水中にいるだけでは安全です。したがって、到着イベントはこれを追跡する必要があります。
class ArrivalEvent...
Port _port; Ship _ship; internal ArrivalEvent (DateTime occurred, Port port, Ship ship) : base (occurred) { this._port = port; this._ship = ship; } internal Port Port {get {return _port;}} internal Ship Ship {get{return _ship;}} internal override void Process() { Ship.HandleArrival(this); }
ここでも、ハンドラーはShipオブジェクトです。
class Ship...
IList cargo; public void HandleArrival (ArrivalEvent ev) { Port = ev.Port; foreach (Cargo c in cargo) c.HandleArrival(ev); }
船はカナダへの訪問を追跡する責任がないため、到着通知を貨物に渡します。
class Cargo...
public bool HasBeenInCanada = false; public void HandleArrival(ArrivalEvent ev) { if (Country.CANADA == ev.Port.Country) HasBeenInCanada = true; }
イベントprocessメソッドがドメインロジックを保持する場合の代替案を考えてみましょう。ドメインロジックが複雑になるにつれて、ドメインモデルに関する多くの知識が必要になります。このアプローチでは、ドメインオブジェクトはイベントを関連オブジェクトに渡して、応答で必要な処理を実行できるようにします。
例:外部システムの更新(C#)
イベントソーシングの優れた機能の1つは、イベントを好きなだけ再処理できることです。ただし、イベントを処理すると外部システムとのインタラクションが発生する場合は、これは悪いことです。最良の事態は、それらがあなたのすべてのスパムイベントに飽きてしまうことです。
この問題を解決する簡単な方法は、システムが外部システムを呼び出す際に、イベントを「実際に」処理している場合を除いてメッセージが送信されないように構成できるゲートウェイを介して呼び出すようにすることです。
今回は船と港(今回は貨物なし)を使用した簡単な例でこれを説明します。船が港に入るたびに、地元の税関当局に通知する必要があるとしましょう。これは、イベント処理ドメインロジックで実現できます。
class Port...
public void HandleArrival (ArrivalEvent ev) { ev.Ship.Port = this; Registry.CustomsNotificationGateway.Notify(ev.Occurred, ev.Ship, ev.Port); }
このコードは、ゲートウェイオブジェクトで通知を呼び出すだけで、これが実際の処理であるか、何らかの再生であるかを気にしないことに注意してください。ここでの一般的な原則は、ドメインロジックはイベントの実行のコンテキストを決して気にするべきではないということです。
実際にメッセージを送信するかどうかを判断するのは、ゲートウェイの責任です。この場合は非常に単純なので、イベントプロセッサへのリンクを持ち、プロセッサがアクティブかどうかを確認するだけでこれを行います。
class CustomsEventGateway...
EventProcessor processor; public void Notify (DateTime arrivalDate, Ship ship, Port port) { if (processor.isActive) SendToCustoms(BuildArrivalMessage(arrivalDate, ship, port)); }
イベントプロセッサは、通常の処理を実行するときに、自身をアクティブにするだけです。
class EventProcessor...
public void Process(DomainEvent e) { isActive = true; e.Process(); isActive = false; log.Add(e); }
このケースは非常に単純ですが、基本的な原則は同じです。ゲートウェイは、外部メッセージを送信するかどうかを決定します。ドメインロジックではありません。ゲートウェイは、処理のコンテキストについて収集した情報に基づいてこれを決定します。この場合、プロセッサからの単純なブール状態だけで十分です。
例:イベントの反転(C#)
ここでは、輸送の例を取り上げ、イベントを反転する方法を見てみましょう。反転に必要な最も重要なことは、イベントによって状態が変化したオブジェクトの前の状態を正確に計算できるようにすることです。
この前のデータを格納するのに適した場所はイベント自体です。これは、ドメインオブジェクト間でイベントを渡すという例のアプローチとうまく機能します。ドメインオブジェクトはイベントを保持しているため、それらのイベントに関する情報を簡単に格納できます。
読み込みイベントは簡単な例です。イベントは、次のソースデータを伝送します。
class LoadEvent...
int _shipCode; string _cargoCode; internal LoadEvent(DateTime occurred, string cargo, int ship) : base(occurred){ this._shipCode = ship; this._cargoCode = cargo; } internal Ship Ship {get { return Ship.Find(_shipCode); }} internal Cargo Cargo {get { return Cargo.Find(_cargoCode); }}
処理はcargoオブジェクトに渡されます。このオブジェクトは、貨物の前の場所であった港を格納する必要があります。
class LoadEvent...
internal override void Process() { Cargo.HandleLoad(this); }
internal Port priorPort;
class Cargo...
internal void HandleLoad(LoadEvent ev) { ev.priorPort = _port; _port = null; _ship = ev.Ship; _ship.HandleLoad(ev); }
イベントを反転するには、ドメインオブジェクトで反転メソッドを呼び出すprocessメソッドをミラーリングする反転メソッドを追加します。
class LoadEvent...
internal override void Reverse() { Cargo.ReverseLoad(this); }
class Cargo...
public void ReverseLoad(LoadEvent ev) { _ship.ReverseLoad(ev); _ship = null; _port = ev.priorPort; }
この場合、イベントは、処理データの一部である可変の以前のデータの束を受け取ります。このような場合、単純なフィールドで十分です。他のケースでは、より洗練されたデータ構造が必要になる場合があります。貨物が到着イベントを処理するとき、カナダに滞在したかどうかを追跡します。これは、単純なブールフィールドで行うことができます。イベントに以前の値を格納するには、多くの場合、到着の影響を受ける可能性があるため、単純なフィールドよりも多くのものが必要です。したがって、この場合は、貨物によってインデックスが付けられたマップを使用します。
class Cargo...
public void HandleArrival(ArrivalEvent ev) { ev.priorCargoInCanada[this] = _hasBeenInCanada; if ("CA" == ev.Port.Country) _hasBeenInCanada = true; } private bool _hasBeenInCanada = false; public bool HasBeenInCanada {get { return _hasBeenInCanada;}}
class ArrivalEvent...
internal Port priorPort; internal IDictionary priorCargoInCanada = new Hashtable();
次に、反転します。
class Cargo...
public void ReverseArrival(ArrivalEvent ev) { _hasBeenInCanada = (bool) ev.priorCargoInCanada[this]; }
この例は、イベントのソースデータとエラー処理が反転方法にどのように影響するかをうまく示しています。ロードイベントの場合、貨物がロードされたときに貨物があった港を格納する必要があります。イベントにこれがソースデータに含まれている場合、これを行う必要はありません。少し余分なソースデータがあれば、前のデータを追加する必要がなくなります。これはどこでも機能するわけではありません。到着イベントは、貨物の以前のカナダの状態を調達することはできません。
正しいと見なされる処理も違いを生む可能性があります。このシステムでは、船の到着イベントと出発イベントの両方があります。すべてが正しく機能すると仮定すると、これらは常にインターリーブされるはずです。したがって、船は、ポートフィールドをPort.OUT_TO_SEA
に設定することにより、到着イベントを反転できます。連続して2つの到着イベントが発生した場合はどうなりますか?これを反転するには、船の前の港を格納する必要があります。別の方法としては、出発を伴わない2回目の到着をエラーとして宣言することです。このストレージを行う必要はありません。
例:外部クエリ(C#)
基本的なイベントソーシングの場合でも、外部クエリは扱いにくいものです。これは、アプリケーションの状態を再構築する場合は、過去に行われた外部クエリの応答を使用して再構築する必要があるためです。
船が港に入る際に貨物の価値を決定しなければならない場合を想像してみましょう。この評価は、外部サービスによって行われます。11月3日に船が港に入り、イベントをすぐに処理すると、11月3日時点の貨物の価値が得られます。12月5日にアプリケーションの状態を再構築する場合、その価値がその間に変化していたとしても、同じ貨物の価値が必要になります。
この例では、外部クエリの記録を保持し、イベントを再処理するときに値を提供することで、これを処理する1つの方法を示します。多くの点で、外部更新で使用するアプローチに似ています。インタラクションをシステムの境界でのイベントに変換し、イベントの記録を使用して発生したことを記憶します。
これらの例の他の場所で使用しているイベント処理と同様のスタイルを使用して、イベントをドメインオブジェクトに渡して処理します。この場合、貨物は外部システムへの呼び出しを開始し、値を保存します。(この値で実際に役立つことを行うと仮定しますが、これはこの例とは関係ありません。)
class Cargo...
public void HandleArrival(ArrivalEvent ev) { declaredValue = Registry.PricingGateway.GetPrice(this); } private Money declaredValue;
私の習慣に従い、私が制御するゲートウェイを介してすべての外部システムアクセスをカプセル化します。基本的なゲートウェイは、外部インタラクションに必要なものを呼び出しに変換するだけです。イベントの再生をサポートするために、これらのクエリをログに記録するクラスでこれをラップします。

図6:イベントからの適切な再構築をサポートするために、ロギングゲートウェイでゲートウェイをラップします。
ロギングゲートウェイは、各呼び出しで、この呼び出しに一致する古いリクエストがあるかどうかを確認し、ない場合は新しいリクエストを作成します。
class LoggedPricingGateway...
public Money GetPrice(Cargo cargo) { GetPriceRequest oldReq = oldRequest(cargo); if (null != oldReq) return (Money) oldReq.Result; else return newRequest(cargo); }
リクエストが新しいリクエストの場合、リクエストをイベントオブジェクトに変換し、外部クエリを呼び出し、ログに保存します。
class LoggedPricingGateway...
private Money newRequest(Cargo cargo) { GetPriceRequest request = new GetPriceRequest(cargo); request.Result = gateway.GetPrice(cargo); log.Store(request); return (Money) request.Result; }
private class GetPriceRequest : QueryEvent { private Cargo cargo; public GetPriceRequest(Cargo cargo) : base() { this.cargo = cargo; }
class QueryEvent...
DomainEvent _eventBeingProcessed; Object result; public QueryEvent() { _eventBeingProcessed = Registry.EventProcessor.CurrentEvent; } public object Result { get { return result; } set { result = value; } } public DomainEvent EventBeingProcessed { get { return _eventBeingProcessed; } } }
したがって、古いリクエストを見つけるために、ログを検索します。
class LoggedPricingGateway...
private GetPriceRequest oldRequest(Cargo cargo) { IList candidates = log.FindBy(EventProcessor.CurrentEvent, typeof (GetPriceRequest)); foreach (GetPriceRequest request in candidates) { if (request.Cargo.RegistrationCode == cargo.RegistrationCode) return request; } return null; }
クエリログは汎用であるため、処理されたドメインイベントとリクエストのタイプを使用して、いくつかのアイテムを取得するクエリを発行できます。これにより、ゲートウェイ固有の方法でさらに確認する必要がある小さなセットが得られます。
リクエストのログは、アプリケーションの状態を再構築するために必要なため、ドメインイベントのログが永続化されるのと同じ方法で永続化する必要があります。