SpringFrameworkによるドメインイベントの実装
ドメインイベントとはドメイン駆動設計において、ドメインエキスパートが気に掛けるなんらかのイベントの事をモデルで表現したものになります。また、複数の集約の状態を同期する事にも用います。ドメインイベントがドメイン内の集約を非同期的に更新する場合、イベントの配送を行うなんらかの実装がなければなりません。今回はSpringFrameworkを使用してイベント配送の仕組みの実装をしてみました。
ドメインイベントとは?
ドメインの中には、なんらかのイベントに関心があるものがあります。例えば、ドメインエキスパートの言葉の中に「〜の場合は通知が欲しい」とあった場合、対象をドメインイベントとしてモデリング、実装する方法が考えられます。一方、ドメインエキスパートからドメインイベントの必要性を感じさせるような言葉がなかったとしても開発者が必要に応じてドメインイベントを定義する場合もあります。
ドメインイベントは下記の「実践ドメイン駆動設計」で詳しく解説されているので興味があれば一読しておくといいでしょう。これからのドメイン駆動設計ではドメインイベントを扱う事が必須となっていくと思われます。
複数の集約は単一のトランザクションで更新してはいけない
Eric Evans氏は集約を以下のように定義しています。
集約とは、関連するオブジェクトの集まりであり、データを変更するための単位として扱われる。
ここでのポイントは集約がデータの変更の単位であるという点です。変更の単位とはすなわちトランザクションの事であり、ソフトウェアを使用するユーザーによって起動させる一連の処理の事を指しています。例えば集約Aと集約Bをどちらも同じトランザクションで変更し保存してはいけません。単一のトランザクションで複数集約の更新を許可するとロックする対象のオブジェクトが多くなり、扱いずらいシステムになってしまうからです。
では、ユーザーの操作で複数集約を更新するにはどうすれば良いのでしょうか?そのためには上記のドメインイベントを使い、結果整合性を担保するようにします。
ドメインイベントによる複数集約の更新
結果整合性は、結果的に一貫性が保たれればよいという考え方に基づいています。これは複数のドメインオブジェクトの状態がトランザクション単位で整合性を持つのではなく、複数のトランザクションで段階的に整合性を確保する事であり、すべてのトランザクションが完了すると整合性が保たれる状態にします。よって、ある一定の期間整合性が担保されていない状態が生まれるという事です。
ドメインイベントを用いた結果整合性による複数集約の更新の例を以下の図に示します。
この例では集約Aがユーザーの操作によって更新され、それによって集約C,Bも更新される事を示しています。集約Aが変更されるとドメインイベントが生成され、ドメインイベントパブリッシャーと呼ばれるイベント配送クラスに渡されます。ドメインイベントパブリッシャーは受け取ったイベントを別のトランザクション(非同期)でドメインイベントサブスクライバに渡します。ドメインイベントサブスクライバは受け取ったイベントの情報を元にそれぞれの集約を更新します。
ドメインイベントパブリッシャーとドメインイベントサブスクライバが、ドメインイベントを用いて複数集約を更新するために必要なイベント配送の仕組みになります。これらをSpringFrameworkを使ってどの様に実装できるのか、解説して行きます。
イベント配送の実装
図書館の業務を例に、以下の「図書貸し出し」に関する複数集約の更新について考えてみます。
これは以下の様な順で処理が発生する事を示しています。
- 「図書」集約が貸し出し中に変化する。
- 「図書が貸し出された」ドメインイベントが発生する。
- 「ユーザーの借りている図書リストを更新する」サブスクライバがイベントをハンドリングする。
- 「ユーザー」集約の借りている図書リストが更新される。
実装にはSpringFrameworkのイベントハンドリングの機能を使います。この機能はコア機能に分類されるので、Springのどのプロジェクトでも使用する事ができます。
まず、ドメインイベントの実装から始めましょう。
SpringのイベントハンドリングではApplicationEventクラスを継承したクラスがイベントオブジェクトとして扱われパブリッシャーを通じサブスクライバに配送されます。ただ、ドメインイベントがこのクラスを直接継承してしまうと、ドメイン層がSpringに依存することになります。これは、ドメイン層を他の技術領域から隔離する観点からすると好ましい構造ではありません。
オススメの実装方法は個々のドメインイベントの共通の関心事をレイヤースーパータイプ、DomainEventとして宣言し、Springへの依存度を下げる事です。以下にドメインイベントクラスの継承階層を示します。
JavaでDomainEventクラスを実装すると以下になります。ここでは共通の関心事をイベントの発生日時として実装しました。各イベントが発生日時を持つ事で、イベントの発生順序を追跡できます。コンストラクタのsourceはイベントを発行したインスタンスを指定します。
DomainEventを継承したBookLentOutEventの実装は以下になります。貸し出された本のIDと借りたユーザーのIDを持っており、ドメインイベント値をイミュータブルとする事で、値が変更されない事を保証します。
次にドメインイベントパブリッシャーの実装です。ドメインイベントパブリッシャーはDomainEventクラスを各ドメインイベントサブスクライバに配送する役割を持っています。Springのイベントハンドリングの機能ではApplicationEventPublisherAwareインターフェイスを実装する事でイベントを配送するApplicationEventPublisherインスタンスを取得できます。この仕組みを使ったドメインイベントパブリッシャーの実装を以下に示します。
SpringはApplicationEventPublisherAwareを実装しているクラスを見つけるとsetApplicationEventPublisherメソッドを呼び出しApplicationEventPublisherインスタンスを引数で渡します。DomainEventPublisherはドメイン層からイベントの配送を受け付けるためstatic領域にApplicationEventPublisherを格納し、publishメソッドでDomainEventの配送を受け付けます。
最後に配送されたドメインイベントのハンドリングをする、ドメインイベントサブスクライバについて説明しましょう。ドメインイベントサブスクライバはApplicationListenerインターフェイスを実装する方法と、アノテーションで実装する二つの方法があります。今回はアノテーションで実装する方法を示します。
EventListenerアノテーションを付与したメソッドはApplicationEventPublisherインスタンスから配送されたイベントをハンドリングする事ができます。ハンドリングする対象のイベントは引数の型で指定します。この例ではBookLentOutEventをハンドリングしたいので、BookLentOutEventを引数にとるメソッドにEventListenerアノテーションを付与します。また、Springでは標準でイベントのハンドリングをシーケンシャルに処理します。つまり同一スレッドで各イベントのハンドリングを行うわけです。となると、単一のトランザクションでイベントを処理することになり、結果整合性を実現できません。そこでAsyncアノテーションを付与し、イベントのハンドリングを別スレッドで行います。以下に例を示します。
ここまでの実装でドメインイベントとその配送の仕組みが整いました。あとは、実際にイベントを発行する処理を集約に実装するだけです。以下に「図書」集約が変更された時にDomainEventPublihserにイベントを発行する例を示します。
終わりに
本稿ではドメインイベントについての簡単な説明とSpringのイベントハンドリングの機能を使った複数集約の同期処理について実装例を提示しながら説明しました。ドメイン駆動設計を進める中で一つの参考になれば幸いです。