事件溯源是領域驅動設計設計想法中的架構模式之一。領域驅動設計是面向業務的一種建模方式。它幫助開發者建立更貼近業務的模型。今天我們就來聊聊php中的事件溯源。
事件溯源(Event Sourcing)是領域驅動設計(Domain Driven Design)設計思想中的架構模式之一。領域驅動設計是面向業務的一種建模方式。它幫助開發者建立更貼近業務的模型。
在傳統的應用程式中,我們將狀態儲存在資料庫中,當狀態改變時,我們即時更新資料庫中相對應的狀態值。事件溯源則採用一種截然不同的模式,它的核心是事件,所有的狀態都來自事件,我們透過播放事件來取得應用程式中的狀態,所以它叫做事件溯源。
在本文中,我們將運用事件溯源模式來寫一個簡化的購物車,以此分解事件溯源的幾個重要組成概念。我們也將使用 Spatie 的事件溯源庫來避免重複造輪。
在我們的案例中,用戶可以添加,刪除以及查看購物車內容,同時它具備兩個業務邏輯:
購物車不可添加超過 3 種產品。當使用者新增第 4 種產品時,系統將自動發出一個預警郵件。
要求以及宣告
本文使用 Laravel 框架。本文使用特定版本 spatie/laravel-event-sourcing:4.9.0 以避免不同版本之間的語法問題。本文並非手把手的分步教程,你必須有一定 Laravel 基礎才能理解本文,請避免咬文嚼字,關注架構模式的組成結構。本文的重點是闡述事件溯源的核心思想,此函式庫中對事件溯源的實作方式並非唯一方案。
領域事件(Domain Event)
事件溯源中的事件稱為領域事件,與傳統的事務事件不同,它有以下幾個特點:
它與業務息息相關,所以它的命名往往夾帶業務名詞,而不應該與資料庫掛鉤。例如購物車增添商品,對應的領域事件應該是 ProductAddedToCart, 而不是 CartUpdated。它是指發生過的事情,所以它一定是過去式,例如 ProductAddedToCart 而不是 ProductAddToCart。領域事件只可追加,不可以刪除或更改,如果需要刪除,我們需要使用具備刪除效果的領域事件,例如 ProductRemovedFromCart。
根據上述信息,我們建構三種領域事件:
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 代表購物車中商品超標,這是我們前面提到的業務邏輯之一。
聚合(Aggregate)
在領域驅動設計中,聚合(Aggregate)是指一組緊密相關的類,他們自成一體形成一個有邊界的組織,邊界外部的物件只可以透過聚合根(Aggregate Root)與此聚合交互,而聚合根是聚合中的一種特殊的類別。我們可以將聚合想像中一個家庭戶口本,對此戶口本進行任何操作,都必須透過戶主(聚合根)。
聚合有以下幾個特點:
它確保核心業務的不變性。也就是說我們在聚合做驗證,對違反業務邏輯的操作拋出例外。它是領域事件的產生地。領域事件在聚合根中產生。也就是說我們可在領域事件已完成業務要求。它自成一體,具有明顯的邊界,也就是說,只能透過聚合根來呼叫聚合中的方法。
聚合是服務於業務邏輯的主要以及最直接的部分,我們使用它直觀地為我們的業務建立模型。
綜上所述,讓我們建立一個 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,分別代表新增以及移除商品。
另外我們還需要加些屬性來記錄購物車內容:
<?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 事件,同時,我們透過魔術方法為 $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中文網其他相關文章!