本文转载自【何以解耦】: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:
<?phpuse 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:
<?phpuse Spatie\EventSourcing\StoredEvents\ShouldBeStored;class ProductRemovedFromCart extends ShouldBeStored{ public int $productId; public function __construct(int $productId) { $this->productId = $productId; }}
- CartCapacityExceeded:
<?phpuse 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
聚合根:
<?phpuse Spatie\EventSourcing\AggregateRoots\AggregateRoot;class CartAggregateRoot extends AggregateRoot{ public function addItem(int $productId, int $amount) { } public function removeItem(int $productId) { }}
CartAggregateRoot
具备两个办法 addItem
和 removeItem
,别离代表增加以及移除商品。
另外咱们还须要加些属性来记录购物车内容:
<?phpuse 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;
将记录购物车中的商品,那么咱们什么时候能够为其赋值呢?在事件溯源中,这是在事件产生当前,所以咱们首先须要公布畛域事件:
<?phpuse 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
赋值:
<?phpuse 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
:
<?phpuse 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 个产品时,零碎将主动收回一个预警邮件。
<?phpuse 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以及简洁代码感兴趣,欢送关注公众号【何以解耦】,一起摸索软件开发之道。