yoskhdia’s diary

DDDとかプログラミングとかアーキテクチャとか雑多に

ドメインイベントを設計する

speakerdeck.com

第3回Reactive System Meetup in 西新宿のLTで発表をしてきました。

reactive-shinjuku.connpass.com

LTという都合上、含めたかったけれど泣く泣く削ったボツネタも併せて補足するエントリです。 (例によって長いです。)

Reactive Systemの文脈でドメインイベントを使うモチベーション

今回のLTの募集要項が「リアクティブに関連すればなんでも」なのに、思いっきりDDDの話じゃん!というのが、もしかしたらあるかもしれませんので、ここで補足しておきます。

f:id:yoskhdia:20160922125132p:plain

引用:The Reactive Manifesto

上図の通りMessage Drivenがリアクティブシステムでの基盤となります。 このMessageは大きく3種類あることはスライドで述べましたが、その内のEvent Messageはリアクティブシステムを考えるうえで非常に重要だなと思っています。 それは、不特定多数にブロードキャストするという性質から疎結合さにつながるからです。
Command - Documentは、従来のメソッド呼び出しを置き換えるようなイメージがしやすいものですが、Eventはちょっと毛色が違います。*1 このEventのもつ疎結合さを上手く活用することが、スケーラビリティや即応性に大きく貢献するのではないでしょうか。

DDDのドメインイベントは、リアクティブシステム云々よりも汎用的なモデリングのためのパターンの一つです。 ドメインイベントを実装する手段として、Event Messageがあると考えています。 つまり、良いEvent Messageを設計するには、ドメインイベントについて学ぶことが良いヒントになると思っています。


ここからはボツネタ集です。

値オブジェクトでまとめる

ドメインイベントのプロパティの型はプリミティブでも良いですが、値オブジェクトにすると表現力が高まります。 プリミティブだと意味が希薄になりますし、振る舞いももたせられません。 また、静的型付け言語を使っている場合は誤った代入を弾ける恩恵も受けられます。 例えば、次のような感じです。*2

case class CustomerInvoiceWritten(
  id: InvoiceId,

  createdUtc: DateTime,
  customer: CustomerId,
  customerName: String,
  customerBillingAddress: string,

  lines: Seq[InvoiceLine],

  subTotal: CurrencyAmount,
  optionalVat: VatInformation,
  varAmount: CurrencyAmount,
  total: CurrencyAmount
)

は、こうなります。

case class CustomerInvoiceWritten(
  id: InvoiceId,
  header: InvoiceHeader,
  lines: Seq[InvoiceLine],
  footer: InvoiceFooter
)

※プロパティの数が少ないなら、無理に値オブジェクトにする必要もありません。適材適所です。

エンティティは含めない

エンティティは本質的にはミュータブルなものです。*3 ドメインイベントはイミュータブルなもので、過去の出来事を表現します。 そのため、エンティティをドメインイベントに含めてしまうと、そのエンティティが更新されたとき矛盾が発生します。 エンティティは識別子が同じであれば、プロパティが異なっても同じモノと判断します。

エンティティをドメインイベントに含めて、スナップショットのようにドメインイベントを使ってはいけません。 スナップショットはあくまでリードモデルとして使うものであり、一方でドメインイベントは綿々と続く流れがあります。 ドメインイベントには、どこで何がどのように変化した、というような出来事(事実)のみを持たせるようにします。

自立性

ローカルの境界づけられたコンテキスト内のみに限れば、情報量(プロパティ)が多くても良いかもしれません。 しかし、ドメインイベントは異なる境界づけられたコンテキストに跨ぐのが一般的であるので、トレードオフを考えながらどこまでの情報を含めるかはバランスをとる必要があります。 また、異なるメッセージング基盤を使うケースや、異なる言語間*4でメッセージをやり取りするケースでは、アプリケーションの自立性が重要です。 次の節も参照してください。

異なる境界づけられたコンテキストと、どう共有するか?

DDDの戦略編が参考になります。 例えば以下のような方法があると思います。

共有カーネル

ドメインイベントおよび値オブジェクトを共有カーネルとして独立させます。 関連するアプリケーションは、共有カーネルを通してドメインオブジェクトの受け渡しが可能になります。

ただし、複雑なドメインでは、値オブジェクトのいくつかに、非常に複雑なビジネスロジックを組み込まなければならないかもしれません。 そんな場合に、単にデシリアライズ時の型安全性のためだけに値オブジェクトを共有カーネルに置くと、脆い設計になってしまいます。 コマンドのデシリアライズに使うシンプルな共有クラスと、コアドメインで使う複雑なクラスを明確に区別しておくことが重要です。(必要に応じて互いに変換)

公表された言語

Canonical Data Modelのように、関連するアプリケーションに依らない共通の言語を定義します。 データ交換用のモデルを分離することで、過度な複雑性や依存性を排除することが可能になります。 例えば、シリアライズしたイベントを標準化するなどです。

ただし、型情報を一緒にデプロイする必要はなくなりますが、フォーマットやデータの整合性検査などが必要になるトレードオフがあります。ドキュメント化することが非常に重要になります。

同期的処理

ドメインイベントは基本的には非同期なものですが、これを同期的に扱いたい(トランザクションに含めたい等の意)場合はどうすれば良いでしょうか。 IDDD本では、アプリケーションサービスでローカルなサブスクライバを作るということが述べられています。 私のなかの理解はこんな使い分けです。

  • 非同期
    EventBusを使う。
    不特定多数に対してブロードキャストして、発行側はその後は関与しない。
  • 同期(待ち合わせ)
    Publisher/Subscriberを使う。
    サブスクライブすることをアプリケーションサービス(またはドメインサービス)が明示的に登録。 トランザクションが終わったら、Subscriberを解除する。*5

ドメインイベントの用途は一つではない

ドメインイベントをモデリングしていくと集約にもできることをスライドでも述べました。 これをどんどん突き詰めていった一つの流派がEvent Sourcingなのかなと思っています。*6

Event Sourcingでは、ドメインイベントの積み重ねによってエンティティの状態を再生します。 ただ、集約の永続化にドメインイベントを使う一方で、様々な出来事を伝える際にもドメインイベントが使われます。 このとき、集約が必要とする情報量だけで考えればドメインイベントの情報は少なく済むかもしれません。 ですが、そのイベントを利用する側でリードモデルプロジェクション*7を作ろうとしたとき、複数のイベントを購読しなければいけなくなる等の複雑さを招くこともあります。

ドメインイベントの情報量を増やすことで、これを大幅に単純化できる可能性があります。

「80%のサブスクライバの要求を満たすだけの情報を含めること」(IDDD p.555)

多くのサブスクライバにはムダな情報になるかもしれないものの、ビュー用のプロジェクションプロセッサには、イベントのデータを十分にもたせておくことが必要な場面もあります。 スライドでも述べましたが、利用側が発行側の集約に追加の問い合わせを行ってしまうと旨味が半減してしまいます。

スライドではドメインイベントのプロパティは小さく始めることに焦点をあてました。 いきなり値オブジェクトをバンバンいれても、それが本当に8割のサブスクライバで必要かは分からない気がするからです。 それなら、小さく始めてドメインイベントも育てていった方が良いと思っています。 丁度よい旨味を得るために、ドメインイベントも蒸留していくことが大切です。

まとめ

ドメインイベントは非常にパワフルです。

ドメインイベントは汎用性のあるパターンです。 イベントと聞くとEvent Sourcingを思い浮かべもしますが、求められるものによって適材適所があります。 ドメインイベントは、そうでないアプリケーションにも有用です。 ミュータブルさを局所化していくことで、モデルの複雑性を低減できる可能性があります。 活用していきましょう。

*1:Pub/Sub、Observerパターンに慣れてる人からするとそうでもないでしょうけれど、処理フローが分断されているという点で。

*2:IDDD本から借用

*3:データモデルというニュアンスで。実装レベルではイミュータブルにする方が良いと思っています。

*4:例えばRubyScala間とか

*5:アプリケーションの作りによって大きく変わります。場合によっては同じSubscriberを使うこともあるかもしれません。

*6:ミュータブルさを極限まで排したアーキテクチャなので、非機能要件・機能要件の全体でマッチするのかを考える必要があると思います。

*7:イベントの写像をつくり、読み込み専用モデルとして永続化すること