本文轉載自【何以解耦】:codedecoupled.com/php-es.html
事件溯源(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
事件,與此同時,我們通過 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 的出現讓大多數開發者的模型停留在了資料庫層面,模型不應該是對資料庫表的封裝,而是對業務的封裝。物件導向程式設計賦予我們的是對業務物件更精確的建模能力。資料庫的設計,資料的操作並不是軟體關注的核心,業務才是。
在軟體設計中,我們應該忘記資料庫設計,將注意力放到業務上面。
本文轉載自【何以解耦】: codedecoupled.com/php-es.html ,如果你也對 TDD,DDD以及簡潔程式碼感興趣,歡迎關注公眾號【何以解耦】,一起探索軟體開發之道。
本作品採用《CC 協議》,轉載必須註明作者和本文連結