(jp) =
この 2 部構成のシリーズでは、私の同僚 フリーク 私たちが取り組んでいるプロジェクトのアーキテクチャについて説明します。 途中で遭遇した問題に対する洞察と答えを共有します。 このパートではシステムの設計について、Freek のパートでは具体的な実装について説明します。
シーンを設定しましょう。
このプロジェクトは、私たちが取り組んできた大規模なものの 1 つです。 最終的には、何十万ものユーザーにサービスを提供し、大量の金融取引を処理し、スタンドアロンのテナント固有のインストールをその場で作成する必要があります.
重要な要件の 1 つは、製品の注文フロー (ビジネスの中核) を簡単にレポートできることと、履歴全体を追跡できることです。
この正面向きのクライアント プロセスに加えて、製品を管理するための複雑な管理パネルもあります。 このコンテキストでは、管理アクティビティの履歴を報告または追跡する必要はほとんどありません。 ここでの主な目標は、使いやすい製品管理システムを持つことです。
「製品管理」と「注文」の概念は、設計上の決定を理解するのに十分明確であると思いますが、明らかにこれはオープンソース プロジェクトではないため、これらの用語を意図的に少しあいまいにしていることを理解していただければ幸いです。私たちが作りました。
最初に、CRUD シリーズを超えた私の Laravel に基づいて、このシステムを設計する方法のアプローチについて説明しましょう。
このようなシステムでは、おそらく 2 つのドメイン グループがあります。 Product
と Order
、およびこれらの両方のドメインを利用する 2 つのアプリケーション: AdminApplication
そして CustomerApplication
.
簡略化されたバージョンは次のようになります。
以前のプロジェクトでこのアーキテクチャを使用して成功したので、私たちは単純にそれに頼ることができました。 ただし、特にこの新しいプロジェクトの場合、いくつかの欠点があります。レポートと履歴追跡が注文プロセスの重要な側面であることを覚えておく必要があります. 単なる副作用としてではなく、コード内でそれらをそのように扱いたいと考えています。
たとえば、アクティビティ ログ パッケージを使用して、注文で何が起こったかについての「履歴メッセージ」を追跡できます。 レポートを生成するために、注文テーブルと履歴テーブルにカスタム クエリを書き始めることもできます。
ただし、これらのソリューションは、コア ビジネスのマイナーな副作用である場合にのみ適切に機能します。 この場合、そうではありません。 そのため、Freek と私は、このプロジェクトの設計を考え出すことを任されました。これにより、レポートと履歴の追跡が、保守しやすく、使いやすく、アプリケーションのコア部分になります。
当然のことながら、上記の要件を満たす素晴らしい柔軟なソリューションであるイベント ソーシングに注目しました。 ただし、無料のものは何もありません。イベント ソーシングでは、単純なことを行うためにかなり多くの追加コードを記述する必要があります。 通常は単純な CRUD アクションでデータベース内のデータを操作しますが、バージョン管理を常に念頭に置きながら、イベントのディスパッチ、プロジェクターとリアクターでの処理について心配する必要があります。
イベント ソース システムが多くの問題を解決することは明らかでしたが、付加価値のない場所であっても、多くのオーバーヘッドが発生します。
これが意味することは次のとおりです。イベントソースを決定した場合、 Orders
からのデータに依存するモジュール Products
モジュールのイベントソースも必要です。そうしないと、無効な状態になる可能性があるためです。 もしも Products
イベント ソースではなく、1 つが削除されたため、再構築できませんでした。 Orders
情報が欠落しているため、もう状態。
そのため、すべてをイベント ソースにするか、この問題の解決策を見つけます。
# すべてのものがイベント ソース?!
いくつかの趣味のプロジェクトでイベント ソーシングをいじってみると、イベント ソーシングによって追加される複雑さを過小評価してはならないことに痛感しました。 さらに、Greg Young は、システム全体のイベント ソーシングはしばしば悪い考えであると述べました。彼は、イベント ソーシングに関する誤解について詳しく説明しており、一見の価値があります!
アプリケーション全体をイベント ソースにしたくないことは明らかでした。 そうするだけでは意味がありません。 唯一の代替手段は、ステートフル システムをイベント ソース システムと組み合わせる方法を見つけることでしたが、驚くべきことに、このトピックに関する多くのリソースを見つけることができませんでした。
それにもかかわらず、私たちはいくつかの労働集約的な調査を行い、私たちの質問に対する答えを見つけることができました. 答えは、イベント ソーシング コミュニティからではなく、確立された DDD プラクティス、つまり境界付けられたコンテキストから得られました。
私たちが望むなら Products
モジュールを独立したステートフル システムにするためには、モジュール間の境界を明確に尊重する必要がありました。 Products
と Orders
. 1 つのモノリシック アプリケーションの代わりに、これら 2 つのモジュールを 2 つの別個のコンテキスト、つまり別個のサービスとして扱う必要があります。 Order
コンテキストが無効な状態になることはありません。
もし Order
に依存しないコンテキストが構築されます。 Product
コンテキストを直接、どのように使用しても問題ありません Product
コンテキストが構築されました。
これについてフリークと話し合ったとき、私は次のように言いました。 Products
別のサービスとして、REST API 経由でアクセスします。 API がオフラインになったり、データ構造が変更されたりした場合でも、イベント ソース アプリケーションが引き続き機能することをどのように保証しますか。
サービスは同じサーバーの同じコードベース内にあるため、サービス間で通信するための API を実際に構築することはありません。 それでも、システムの設計を開始したのは良い考え方でした。
境界は次のようになります。各サービスには独自の内部設計があります。
CRUD シリーズを超えた私の Laravel を読んでいるなら、すでに Product
コンテキストが機能します。 そこには何も新しいことが起こっていません。 の Order
ただし、コンテキストにはもう少し背景情報が必要です。
# イベントソースの一部
では、イベント ソースの部分を見てみましょう。 この記事を読んでいるあなたは、少なくともイベント ソーシングに興味があると思うので、すべてを詳しく説明することはしません。
の OrderAggregateRoot
このコンテキスト内で発生するすべてを追跡し、アプリケーションが対話するためのエントリ ポイントになります。 また、すべてのリアクタとプロジェクタに保存および伝播されるイベントをディスパッチします。
リアクターは決して再生されない副作用を処理し、プロジェクターは投影を行います。 私たちの場合、これらは単純な Laravel モデルです。 これらのモデルは、プロジェクター内からのみ書き込むことができますが、他のコンテキストから読み取ることができます。
ここで行った設計上の決定の 1 つは、読み取りモデルと書き込みモデルを分割しないことでした。 と これらのモデルはプロジェクターを介してのみ書き込まれるという書面による慣習。 そのような投影モデルの一例は、 Order
.
覚えておくべき最も重要なルールは、 Order
コンテキストは、保存されたイベントからのみ再構築できる必要があります。
では、他のコンテキストからデータを取り込むにはどうすればよいでしょうか。 どうすれば Order
コンテキスト内で何かが発生したときに通知される Product
それに関連する文脈? 1つ確かなことは、関連するすべての情報 Products
内にイベントとして保存する必要があります。 Order
環境; そのコンテキスト内では、イベントが唯一の真実の情報源であるためです。
これを実現するために、3 番目の種類のイベント リスナーを導入しました。 プロジェクターとリアクターはすでにあります。 次に、サブスクライバーの概念を追加します。 これらのサブスクライバーは、他のコンテキストからのイベントをリッスンし、現在のコンテキスト内で適切に処理することができます。 ほとんどの場合、ほとんどの場合、外部イベントを内部の保存されたイベントに変換します。
イベントが Order
コンテキスト、への依存関係を安全に忘れることができます Product
環境。
一部の読者は、これら 2 つのコンテキスト間でイベントをコピーすることによってデータを複製していると考えるかもしれません。 もちろん、 Orders
特定のイベント Product
だった created
、はい、一部のデータがコピーされます。 しかし、これにはあなたが思っている以上の利点があります。
まず第一に: Product
context は、他のどのコンテキストがそのデータを使用するかについて何も知る必要はありません。 イベントは保存されないため、イベントのバージョン管理を考慮する必要はありません。 これにより、 Product
複雑なイベントソーシングが追加されることなく、通常のステートフルアプリケーションであるかのようにコンテキストを使用できます。
2 つ目: だけではありません。 Order
イベントソースのコンテキストであり、これらのコンテキストはすべて、内部でトリガーされた関連イベントを個別にリッスンできます。 Product
環境。
3 つ目: オリジナルの完全なコピーを保存する必要がない Product
各コンテキストは、独自のユースケースに関連するデータをチェリーピックして保存できるためです。
# データの移行はどうですか?
新たな疑問が生じました。
このシステムが 1 年間運用されていて、イベント ソースの新しいコンテキストを追加することにしたとします。 についての知識も必要なもの Product
環境。 オリジナル Product
上記の理由により、イベントは保存されませんでした。では、新しいコンテキストの初期状態を構築するにはどうすればよいでしょうか?
答えは次のとおりです。展開時に、すべての製品データを読み取り、既存の製品に基づいて、関連するイベントを新しく追加されたコンテキストに送信する必要があります。 この 1 回限りの移行には追加コストがかかりますが、これにより、 Product
外部を気にする必要はありません。 このプロジェクトにとって、それは支払う価値のある代償です。
# 最終統合
最後に、読み取り専用モデルを使用して、すべてのコンテキストから収集されたデータをアプリケーションで使用できるようになりました。 繰り返しますが、私たちの場合と現在のところ、これらのモデルは慣例により読み取り専用です。 将来的に変更する可能性があります。
アプリケーションから Product
context は、通常のステートフル アプリケーションが行うように行われます。 アプリケーションと、次のようなイベント ソース コンテキストとの間の通信 Orders
集約ルートを介して行われます。
では、最後に総括です。 この図にはまだいくつかの矢印が欠けていますが、コンテキストとアプリケーションの間および内部の関連する流れが明確になることを願っています。
この問題を解決する鍵は、DDD の境界付けられたコンテキストに注目することでした。 それらは、コードベース内の厳密な境界を表しています。つまり、必要なときに簡単に越えることができない境界です。 確かに、これは複雑さの層を追加しますが、他のサポートについて心配することなく、必要な方法で各コンテキストを構築する自由も追加します.
パズルの最後のピースは、コンテキスト間の通信手段としてイベントのみに依存することでした。 繰り返しになりますが、複雑さのレイヤーが追加されますが、デカップリングと柔軟性の手段も追加されます。
それでは、Laravel プロジェクト内でこれをどのようにプログラムしたかを深く掘り下げてみましょう。 パート 2 の同僚の Freek です。