首頁 >後端開發 >PHP問題 >聊聊php中的事件溯源

聊聊php中的事件溯源

醉折花枝作酒筹
醉折花枝作酒筹轉載
2021-07-06 15:26:451631瀏覽

事件溯源是領域驅動設計設計想法中的架構模式之一。領域驅動設計是面向業務的一種建模方式。它幫助開發者建立更貼近業務的模型。今天我們就來聊聊php中的事件溯源。

聊聊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(&#39;product_id&#39;, $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(&#39;admin@corporation.com&#39;)->send(new CartWarning());
    }
}

反应机 WarningReactor 

会监听到事件 CartCapacityExceeded, 我们就会使用 Laravel Mailable 发送一封警报邮件。

总结

至此我们简单的介绍了事件溯源的几个组成部分。软件的初衷是运用我们熟悉的编程语言来解决复杂的业务问题。为了解决现实中的业务问题,大神们发明了面向对象编程(OOP),于是我们可以避免写出面条代码,可以建立最贴近现实的模型。但是由于某种原因, ORM 的出现让大多数开发者的模型停留在了数据库层面,模型不应该是对数据库表的封装,而是对业务的封装。面向对象编程赋予我们的是对业务对象更精确的建模能力。数据库的设计,数据的操作并不是软件关注的核心,业务才是。

在软件设计之初,我们应该忘记数据库设计,将注意力放到业务上面。

推荐学习:php视频教程

以上是聊聊php中的事件溯源的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:何以解耦。如有侵權,請聯絡admin@php.cn刪除