共计 5788 个字符,预计需要花费 15 分钟才能阅读完成。
本文转载自【何以解耦】:https://codedecoupled.com/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
事件,与此同时,咱们通过 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 的呈现让大多数开发者的模型停留在了数据库层面,模型不应该是对数据库表的封装,而是对业务的封装。面向对象编程赋予咱们的是对业务对象更准确的建模能力。数据库的设计,数据的操作并不是软件关注的外围,业务才是。
在软件设计之初,咱们应该遗记数据库设计,将注意力放到业务下面。
本文转载自【何以解耦】:https://codedecoupled.com/php…,如果你也对 TDD,DDD 以及简洁代码感兴趣,欢送关注公众号【何以解耦】,一起摸索软件开发之道。