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

聊聊php中的事件溯源

Jul 06, 2021 pm 03:26 PM
事件溯源

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

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1665
14
CakePHP 教程
1424
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24