关于ddd:PHP-事件溯源

5次阅读

共计 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;}

}

事件 ProductAddedToCartProductRemovedFromCart 别离代表商品退出购物车以及被从购物车中移除,事件 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 具备两个办法 addItemremoveItem,别离代表增加以及移除商品。

另外咱们还须要加些属性来记录购物车内容:

<?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)
        );
    }

}

在调用 addItemremoveItem 事件时,咱们别离公布 ProductAddedToCartProductRemovedFromCart 事件,与此同时,咱们通过 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_cartsProjectionCart 是一个一般的 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 以及简洁代码感兴趣,欢送关注公众号【何以解耦】,一起摸索软件开发之道。

正文完
 0