イベント ソーシングは、ドメイン駆動設計思考におけるアーキテクチャ パターンの 1 つです。ドメイン駆動設計は、ビジネス指向のモデリング アプローチです。開発者がビジネスに近いモデルを構築するのに役立ちます。今日はphpでのイベントソーシングについてお話します。
イベント ソーシングは、ドメイン駆動設計の設計哲学におけるアーキテクチャ パターンの 1 つです。ドメイン駆動設計は、ビジネス指向のモデリング アプローチです。開発者がビジネスに近いモデルを構築するのに役立ちます。
従来のアプリケーションでは、状態をデータベースに保存し、状態が変化すると、データベース内の対応する状態値を直ちに更新します。イベント ソーシングはまったく異なるモデルを採用しており、その核となるのはイベントであり、すべての状態はイベントから派生します。イベントを再生することでアプリケーションの状態を取得するため、イベント ソーシングと呼ばれます。
この記事では、イベント ソーシング モデルを使用して簡略化されたショッピング カートを作成し、イベント ソーシングのいくつかの重要な概念を詳しく説明します。また、車輪の再発明を避けるために、Spatie のイベント ソーシング ライブラリも使用します。
この場合、ユーザーはショッピング カートの内容を追加、削除、表示できます。これには 2 つのビジネス ロジックがあります。
ショッピング カートには 3 つを超える製品を追加できません。ユーザーが 4 番目の製品を追加すると、システムは自動的に警告メールを送信します。
要件と宣言
この記事では Laravel フレームワークを使用します。この記事では、異なるバージョン間の構文の問題を回避するために、特定のバージョン spatie/laravel-event-sourcing:4.9.0 を使用します。この記事はステップバイステップのチュートリアルではありません。この記事を理解するには、Laravel に関する一定の基本知識が必要です。冗長な言葉は避け、アーキテクチャ パターンの構造に焦点を当ててください。この記事の焦点は、イベント ソーシングの核となるアイデアを詳しく説明することですが、このライブラリでのイベント ソーシングの実装が唯一の解決策ではありません。
#ドメイン イベント
イベント トレースのイベントはドメイン イベントと呼ばれ、従来のトランザクション イベントとは異なり、次の特徴があります。ビジネスに関連するため、その名前にはビジネス名詞が含まれることが多く、データベースにリンクしないでください。たとえば、ショッピング カートに製品を追加する場合、対応するドメイン イベントは CartUpdated ではなく ProductAddedToCart である必要があります。これは起こったことを指すため、ProductAddToCart ではなく ProductAddedToCart など、過去形にする必要があります。ドメイン イベントは追加のみ可能で、削除や変更はできません。削除する必要がある場合は、ProductRemovedFromCart などの削除効果のあるドメイン イベントを使用する必要があります。 上記の情報に基づいて、次の 3 つのドメイン イベントを構築します。 ProductAddedToCart:<?php use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class ProductAddedToCart extends ShouldBeStored { public int $productId; public int $amount; public function __construct(int $productId, int $amount) { $this->productId = $productId; $this->amount = $amount; } }
ProductRemovedFromCart:
<?php use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class ProductRemovedFromCart extends ShouldBeStored { public int $productId; public function __construct(int $productId) { $this->productId = $productId; } }
CartCapacityExceeded:
<?php use Spatie\EventSourcing\StoredEvents\ShouldBeStored; class CartCapacityExceeded extends ShouldBeStored { public array $currentProducts; public function __construct(array $currentProducts) { $this->currentProducts = $currentProducts; } }イベント ProductAddedToCart と ProductRemovedFromCart は、それぞれショッピング カートに追加されたアイテムとショッピング カートから削除されたアイテムを表します。イベント CartCapacityExceeded は、ショッピング カート内のアイテムが制限を超えていることを表します。これはビジネスの 1 つです。先ほど述べたロジック。
集約
ドメイン駆動設計では、集約とは、境界のある組織を形成する密接に関連したクラスのグループを指します。境界の外側にあるオブジェクトは、この集約とのみ対話できます。集約ルート (集約ルート) を経由します。集約ルートは集約内の特別なクラスです。集計は家族の戸籍簿にたとえられますが、この戸籍簿に対する操作は必ず世帯主(集計ルート)を経由する必要があります。 集約には次の特徴があります: これにより、中核となるビジネスの不変性が保証されます。つまり、集計で検証を実行し、ビジネス ロジックに違反する操作に対して例外をスローします。ここでドメイン イベントが発生します。ドメイン イベントは集約ルートで生成されます。つまり、ドメイン イベントでビジネス要件を完了できます。これは自己完結型であり、明確な境界があります。つまり、集約内のメソッドは集約ルート経由でのみ呼び出すことができます。 集約はビジネス ロジックを提供する主要かつ最も直接的な部分であり、私たちはこれを使用してビジネスを直感的にモデル化します。 要約すると、CartAggregateRoot 集約ルートを構築しましょう:<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { public function addItem(int $productId, int $amount) { } public function removeItem(int $productId) { } }CartAggregateRoot には、それぞれ商品の追加と削除を表す addItem とremoveItem という 2 つのメソッドがあります。 さらに、ショッピング カートの内容を記録するためにいくつかの属性を追加する必要があります:
<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { private array $products; public function addItem(int $productId, int $amount) { } public function removeItem(int $productId) { } }private array $products; はショッピング カート内の商品を記録します。それに価値があるのか?イベント ソーシングでは、これはイベントの発生後であるため、最初にドメイン イベントを発行する必要があります:
<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { private array $products; public function addItem(int $productId, int $amount) { $this->recordThat( new ProductAddedToCart($productId, $amount) ); } public function removeItem(int $productId) { $this->recordThat( new ProductRemovedFromCart($productId) ); } }addItem イベントとremoveItem イベントを呼び出すときに、ProductAddedToCart イベントと ProductRemovedFromCart イベントをそれぞれ発行します。 apply を渡します マジック メソッドは $products に値を割り当てます:
<?php use Spatie\EventSourcing\AggregateRoots\AggregateRoot; class CartAggregateRoot extends AggregateRoot { private array $products; public function addItem(int $productId, int $amount) { $this->recordThat( new ProductAddedToCart($productId, $amount) ); } public function removeItem(int $productId) { $this->recordThat( new ProductRemovedFromCart($productId) ); } public function applyProductAddedToCart(ProductAddedToCart $event) { $this->products[] = $event->productId; } public function applyProductRemovedFromCart(ProductRemovedFromCart $event) { $this->products[] = array_filter($this->products, function ($productId) use ($event) { return $productId !== $event->productId; }); } }apply* は Spatie のイベント ソーシング ライブラリに付属するマジック メソッドです。recordThat を使用してイベントを発行すると、apply* が自動的に呼び出されます。ステータスの変更がイベントのリリース後に行われるようにします。
现在 CartAggregateRoot 已通过事件获取了需要的状态,现在我们可以加入第一条业务逻辑:购物车不可添加超过 3 种产品。
修改 CartAggregateRoot::addItem,当用户添加第 4 种产品时,发布相关领域事件 CartCapacityExceeded:
public function addItem(int $productId, int $amount) { if (count($this->products) >= 3) { $this->recordThat( new CartCapacityExceeded($this->products) ); return; } $this->recordThat( new ProductAddedToCart($productId, $amount) ); }
现在我们已经完成了聚合根工作,虽然代码很简单,但是根据模拟业务而建立的模型非常直观。
加入商品时,我们调用:
CartAggregateRoot::retrieve(Uuid::uuid4())->addItem(1, 100);
加入商品时,我们调用:
CartAggregateRoot::retrieve($uuid)->removeItem(1);
放映机(Projector)
UI 界面是应用中不可缺少的部分,比如向用户展示购物车中的内容,通过重播聚合根或许会有性能问题。此时我们可以使用放映机(Projector)。
放映机实时监控领域事件,我们通过它可以建立服务于 UI 的数据库表。放映机的特点是它可以重塑,当我们发现代码中的 bug 影响到 UI 数据时,我们可以重塑此放映机建立的表单。
让我们写一个服务于用户的放映机 CartProjector:
<?php use Spatie\EventSourcing\EventHandlers\Projectors\Projector; class CartProjector extends Projector { public function onProductAddedToCart(ProductAddedToCart $event) { $projection = new ProjectionCart(); $projection->product_id = $event->productId; $projection->saveOrFail(); } public function onProductRemovedFromCart(ProductRemovedFromCart $event) { ProjectionCart::where('product_id', $event->productId)->delete(); } }
放映机 CartProjector
会根据监听的事件来增加或者删除表单 projection_carts,ProjectionCart 是一个普通的 Laravel 模型,我们仅使用它来操作数据库。
当我们的 UI 需要展示购物车中的内容时,我们从 projection_carts 读取数据,这和读写分离有异曲同工之妙。
反应机(Reactor)
反应机(Reactor)和放映机一样,实时监控领域事件。不同的是反应机不可以重塑,它的用途是用来执行带有副作用的操作,所以它不可以重塑。
我们使用它来实现我们的第二个业务逻辑:当用户添加第 4 个产品时,系统将自动发出一个预警邮件。
<?php use Spatie\EventSourcing\EventHandlers\Reactors\Reactor; class WarningReactor extends Reactor { public function onCartCapacityExceeded(CartCapacityExceeded $event) { Mail::to('admin@corporation.com')->send(new CartWarning()); } }
反应机 WarningReactor
会监听到事件 CartCapacityExceeded, 我们就会使用 Laravel Mailable 发送一封警报邮件。
总结
至此我们简单的介绍了事件溯源的几个组成部分。软件的初衷是运用我们熟悉的编程语言来解决复杂的业务问题。为了解决现实中的业务问题,大神们发明了面向对象编程(OOP),于是我们可以避免写出面条代码,可以建立最贴近现实的模型。但是由于某种原因, ORM 的出现让大多数开发者的模型停留在了数据库层面,模型不应该是对数据库表的封装,而是对业务的封装。面向对象编程赋予我们的是对业务对象更精确的建模能力。数据库的设计,数据的操作并不是软件关注的核心,业务才是。
在软件设计之初,我们应该忘记数据库设计,将注意力放到业务上面。
推荐学习:php视频教程
以上がphp でのイベント ソーシングについて話しましょうの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。