20
12

More than 3 years have passed since last update.

Aecor による純粋関数型イベントソーシング

Last updated at Posted at 2019-03-05

Aecor という純粋関数型 Event Sourcing ライブラリをざっくり紹介したい。

概要

  • Aecor は Denis Mikhaylov というロシアの技術者が開発した、Scala の純粋関数型イベントソーシングライブラリ。作者が勤務するオンライン予約会社の Evotor 社では、Arcorベースの数十のサービスが実運用されているという。
  • ラテン語で海の意味で、あえて五十音で近似すると、ラテン語読みだとアエコル、英語読みだとエイカーみたいな感じ。
  • 純粋関数型DDD に親和性が高い。OOP とは逆に、FP ではデータと振る舞いを分離することが基本となるが1、この辺りは関数型 DDDでも同様。分離の仕方にはいくつかあるが2 3、Aecor 流では Tagless Final で実現する。
  • Typelevel エコシステムのライブラリ、Cats, Cats Effect, FS2 などを多用。MTLTagless Final など中〜上級の関数型プログラミング技法も使われてるので、それなりの素養は必要になる。
  • Akka というか Actor が嫌いな人でも大丈夫。裏では Akka Cluster Sharding が動いているが、FP的に残念な部分は隔離されているので、タイプフルで純粋な関数型コードが書ける。

以降、Aecor の Github のコード例 と、Mikhaylov 氏の同僚の Vladimir Pavkin 氏による連載チュートリアルを参考にしつつ、ややつまみ食い的に解説する。

構成要素

Aecor の構成要素としては以下のようなものがある。

  1. エンティティの振る舞いに着目して高い抽象レベルでドメインモデルを記述する Behavior
  2. 分散コンピューティング環境で Behavior を実行する Runtime
  3. CQRS のリードサイド
  4. ビジネスプロセスを Process Manager パターンで処理する Distributed Processing

この記事では、ES+CQRS に関連する 1 〜 3についてざっくり説明する。

Behavior

ドメイン層では、アイデンティティとライフサイクルをもつエンティティ(特に適切に境界付けられたアグリゲートのルートエンティティ)が主役になるが、Aecor ではエンティティの振る舞い=Behavior に着目してモデリングし4、プログラムの「記述と実行」をきっちり分離した上で、記述部分のみをドメイン層に置く。CQRS で言えば ライトサイドの振る舞いになる。5

以下のような構成要素がある

振る舞い代数 / Behavior Algebra

エンティティの動詞を、Tagless final のスタイルで、振る舞い代数 (behavior algebra)として定義したもの。データは持たない。入門レベルの Tagless Final の記事などでは、F[_]に当たるものとして、せいぜいIdFutureTryくらいで説明されることが多いが、Aecor では MTL と組み合わせて、もっと豊かな型表現として活用されている(サンプル)。

状態 / State

関数型プログラミングでは、データはデータとして、振る舞いと癒着させずにシンプルに表現することになるが、Aecor のドキュメントでは、これを状態(State)と呼んでいる。クラシカルな DDD と同様にデータベース都合のモデリングは厳禁で、Aecor でも Persistence Ignorant なスタイルが尊重される。ちなみに、生成/更新日時といったメタデータやエンティティIDなどもドメイン層に属するものとは見なさず、状態には含めない(後述)。

初期状態を作る関数や、Event を受けて状態遷移させる関数も状態/State のモジュールに書く(サンプル)。これらはランタイムに渡すEventsourcedBehavior インスタンスを生成するときに、後述のアクションと共に必要になる。

イベント / Domain Event

イベントソーシングだけに当然イベントは重視される。Aecor 仕様ではないが、Event Storming セッションなどで、関係者一同で合意できるユビキタス言語として導き出すのが望ましい。

イベントは分散処理のノード間、またノードとジャーナルとの間でもやり取りされるので、後述のインフラ層の作業としてシリアライゼーションが実装されることになる。

却下 / Rejection

例えば、すでに拒否されている予約申し込みへのキャンセルのように、状態とイベントの組み合わせによってはビジネスロジックとして受け付けられないものがある。これを却下= rejection として列挙し理由を表現する(サンプル)。

アクション / Eventsourced Behavior

Tagless Final の入門記事では、代数を定義したら、具体的なエフェクトF[_]を決めてインタープリターを書く流れになることが多いが、Aecor では F[_]をパラメータとしたまま、暗黙に与えられる MonadAction インスタンスへの操作を使って、アクションを記述する(サンプル)。具体的な永続化層へのアクセスはランタイム層のプラグインなどに任せることになる。

MonadAction とその派生クラスは以下のような構成になっていて、ここまでに説明した状態(S)、イベント(E)、却下(R)と、効果F[_]I[_]が、ここで型として合成される。

// 基本形
trait MonadAction[F[_], S, E] extends Monad[F] {
  def read: F[S]                        // 型Sの状態を読む
  def append(es: E, other: E*): F[Unit] // 型Eのイベントを追加する
  def reset: F[Unit]                    // リセットする
}
// 状態遷移の却下を加味したもの
trait MonadActionReject[F[_], S, E, R] extends MonadAction[F, S, E] {
  def reject[A](r: R): F[A]             // 却下理由Rで却下する
}
// 例えば Cats Effect の Clock に与えるような、Task や IO などの単純なF[_]と、
// モナドトランスフォーマーの合成結果となる I[_]を分けて扱いたい場合はこれを使う
trait MonadActionLift[I[_], F[_], S, E] extends MonadAction[I, S, E] {
  def liftF[A](fa: F[A]): I[A]          // F[_] から I[_] にリフトする(モナドトランスフォーマー的な意味で)
}
// 上の Monad*** をすべて併せたもの
trait MonadActionLiftReject[I[_], F[_], S, E, R]
    extends MonadActionLift[I, F, S, E]
    with MonadActionReject[I, S, E, R]

ここまでで、アクション、初期状態、状態遷移関数が出そろったので、EventsourcedBehavior のインスタンスを生成して、ランタイムにデプロイすることが出来るようになった。

※ 詳細は Aecor Github のサンプル、チュートリアルの Part 1, Part 2 など参照。またチュートリアルPart 2では、EventsourcedBehavior の中で動いている ActionT、EitherK、その他の MTL 操作などについても解説されている。

Runtime

上述のイベントソーストな振る舞いを具体的に実行する層。以下のような要素がある。

ランタイム

ドメイン層で定義した振る舞いのデプロイ先をランタイム=Runtimeという。

2タイプのランタイム、Akka Persistence RuntimeGeneric Akka Runtime が提供されている。前者は READMEのサンプルコードでは使われているもののやや古く、後者の Akka Cluster のみを使った Akka Persistence に依存しないランタイムの方がより新しく柔軟な実装で、チュートリアルではこちらが推奨されている。

ジャーナル

シリアライズされたイベントが、アペンドオンリーで永続化されるデータストアがジャーナルで event log とも呼ばれる。postgres + doobie のプラグイン が提供されている。

ノード

スケーラブルな分散 ES/CQRS のために、裏で Akka Cluster Sharding を使っている(ただし前述の通り、Aecor のユーザは Actor に触れることはなく、純粋関数型な ES+CQRS、関数型DDDが可能)。Single Writer Principle 6が成立しているため、個別のエンティティごとに見ると順序は保証されていて、これが後述の CQRS リードサイドでも重要になる。

上で触れた Eventsourced Behavior のインスタンスは、Aecor によってノードにデプロイされることになる。

ワイアリング と シリアライズ

用語的には、ジャーナル-ノードの間でイベントの読み書きに使われるのがシリアライズで、ノード間の通信でのエンコード/デコードをワイヤプロトコルという。

  • シリアライズには Protocol Bufferなどが使える
  • ワイヤプロトコルには、BooPickle 用の WireProtocol を生成するマクロアノテーションが Aecor で提供されている。永続化されるものでもなく、従って人が読める形式である必要がないため、スピードとフットプリントを優先しての BooPickle 採用らしい。

エンティティID とメタデータ

エンティティID とメタデータは、実行環境プロダクトに依存するわけではないが、どちらかというと Behavior よりは Runtime に近い位置付けになる。

メタデータ

イベント生成日時など、ジャーナルには保存されるがドメイン層では扱わない7情報は、メタデータとして別途定義し、Enrichという封筒状のコンテナにイベントと一緒に入れて、インフラ層で扱う。

エンティティID

エンティティIDそれ自体は、他のサブドメインからは参照されうる点で、ドメイン層に属すると言えなくもないが、自分自身のドメインではデータとしても振る舞いとしても使われることがない。

つまりドメイン層の Behavior がアクセスされる時点では、すでに「どのエンティティか?」は定まっているので ID は本来不要なはずであり、逆にエンティティが自分のIDを知る必要があるようならば、関心事が混在 している可能性があるので、アグリゲートの境界を見直す余地がある。

Aecor ではこのポリシーを separate identity and behavior8 と呼び、これに則してエンティティIDを異なる関心事としてドメイン層の振る舞いから分離し、もっぱらジャーナルやワイアリングなどランタイム周りの層で取り扱うことになる。

CQRS (のリードサイド)

以下のような概念と構成要素がある。

射影

ジャーナルをイベントの無限ストリームと捉えて、これに任意の畳込み演算 = fold を施したものを projection = 射影とよび、これを secondary facts として the source of truth としてのジャーナルに対置する。このうちクエリーに最適化されたものをビューと呼び、これが CQRS のリードサイドになる。

ジャーナルからイベントのストリームを得る機能は、ジャーナルのプラグインが提供する。 arcor-postgres-journal プラグインでは fs2 の Stream として提供される。

タギング

タギング という機能で、各イベントにタグがつけられる。Paritioned Tagging を使うと、Kafka のパーティションに相当する形で、イベントストリームを分割して分散処理できるようになる。

オフセット

Projection の畳み込みがどこまで済んだかを示す位置がオフセットで、それを記録するためのストレージをオフセットストアと呼ぶ。これも arcor-postgres-journal で提供されている。

イベント・ストリームの各要素は Committable にラップされていて、これをコミットすることでオフセットも永続化されることになる。

ビューとビュー・リポジトリ

前述の通りクエリー用途の射影がビューで、それを永続化するストレージがビュー・リポジトリ

ビューの更新処理は、現状のビューの上に未処理のイベントを畳み込んで、処理したところまでオフセットストアに書き込んでいくのが基本になる。RDB に限ったものではなく、NoSQL でも分析エンジンでも何でもいい。

リポジトリの実装面では、Tagless Final を使うとライトサイドの Behavior の実装との
整合感が出るが、その場合、欲しいビューに応じた自前のインタープリターを書くことになる。またオフセットのコミットが非同期になるため、若干ややこしい重複除去処理などが生じるが、この辺りはある程度 Aecor でサポートされていて、サンプルコードでもよく分かるようになっている。

ちなみにレイヤーで言えば、CQRSリードサイドのクエリーはほぼクライアント都合で決まる面が多く、ドメイン層というよりアプリケーション層またはユースケース層に属することになるが、個人的にはこの辺はライトサイドほど丁寧には関心事やレイヤーを分離しなくても、さほど問題ない気がしている。

★ 実務上は、ジャーナルをリプレイしてビューを再構築できる仕組みが、やはりとても重要だという。

所感

関数型 DDD、関数型ES+CQRSの一つのあり方、あるいは Tagless final の応用としてとても参考になる。

関連資料など


  1. 例えばこのブログ記事でも言われているように。 

  2. たとえば この記事で解説したように 『Domain Modeling Made Functional』では強く型付けられたワークフローとして振る舞いを表現している。 

  3. 『Functional Reactive Domain Modeling』では Free Monad を活用して、データと振る舞いを分離していた。 

  4. Pavkin 氏のチュートリアルでは、behavior land とも表現されている。 

  5. ちなみに、このライブラリに限定した話ではないが、なんでもかんでもイベントソーシング対象にするのは間違い。 

  6. In simple words it means, that at any point in time in the whole cluster there is maximum one instance of each specific entity, that can process commands and write events to the log. There should be no concurrent command processing and no concurrent writes to the log for any single entity instance. (part 3

  7. クラシカルなDDDでも生成日時、生成ユーザ、更新日、更新ユーザ、削除フラグなどDB都合の要素をドメインモデルに入れない方が良いのと同じこと。 

  8. あるいは "entity should not need identity information to handle commands (part 1)"。または "entity behavior should not know about its identity, it's behavior should be defined solely by its state. (README)" 

20
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
12