11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

如何设计一款大型 Laravel 应用程序的架构(下)?

  • 2020-06-02
  • 本文字数:5156 字

    阅读完需:约 17 分钟

如何设计一款大型Laravel应用程序的架构(下)?


第一篇——《如何设计一款大型 Laravel 应用程序的架构(上)?


本文最初发布于 Freek.dev 博客,经原作者授权由 InfoQ 中文站翻译并分享。


我与同事Brent一起,负责设计一款大型 Laravel 应用程序的架构。我们将这款应用程序分为传统部分和事件溯源部分。在这篇文章中,我想举一个实际例子来说明我们如何实现这一目标。


这是本系列文章中的第二篇。阅读本文前,请先阅读第一部分。此外,你还应该对事件溯源(event sourcing)、聚合和 projector 的作用有很好的理解。这篇介绍可以帮助你快速上手。


设置阶段

首先,我们快速回顾一下 Brent 的文章。我们的应用程序需要事件溯源。在应用的某些部分,我们需要根据过去的信息做出决策;并且随着时间的推移,我们需要能够生成报告,而无需事先了解这些报告中应该有什么内容。


这些问题可以通过事件溯源来解决。我们只存储发生在应用程序中的所有事件,因此我们可以使用聚合(aggregate)根,这些根可以根据事件中的过去信息来做出决策。我们可以创建 projector 并向其回放所有记录的事件,从而生成新报告。


一位名叫 Frank de Jonge 的聪明人曾说过:“事件溯源会让困难的部分变得简单,而让简单的部分变困难”,这说得很对。设置事件溯源需要花费一些时间,而且没那么容易。当你想做一些简单的事情时,还会遇到一些额外的开销。


在我们的应用程序中,我们还有一些相当粗糙的部分,它们不需要事件溯源的能力。在单个应用程序中同时获得两种方法的好处不是很好吗?我和 Brent 研究了这个主题,并结对编程做了一些实验。我们想分享其中一次实验的情况。


一个实际的例子

我们将实现 Brent 文章中提到的两个部分:产品和订单。Product 是很无聊的部分。我们在这里不需要历史记录或报告。对于 Order 部分,我们将使用事件溯源。


你可以在 GitHub 的这个仓库中找到示例的源代码。请记住,这是一个非常简单的示例。在实际应用中所涉及的逻辑可能会更复杂。在这个实验中,我们只是专注研究这样的部分应该/将要如何相互通信。


非事件溯源的部分

查看 Context 目录,你会看到两个部分:Order 和 Product。我们先看一下“Product”:



如你所见,这部分非常简单:这里只有模型和事件。这里的想法是,我们可以像以往那样构建它,不会有太多开销。我们唯一要做的就是在发生某些更改时触发特定事件,以便应用的其他部分能参与其中。


在 Product 模型中,你将看到以下代码


protected $dispatchesEvents = ['created' => ProductCreatedEvent::class,'updated' => ProductUpdatedEvent::class,'deleting' => DeletingProductEvent::class,'deleted' => ProductDeletedEvent::class,];
复制代码


$dispatchesEvents中,你可以定义当模型发生某些事情时应触发的事件。这是标准的 Laravel 功能。当然,我们可以使用原生触发的 Eloquent 事件,但这里我们想要使用自己的事件类,这样才能明确说明问题。


让我们看一下其中一个事件


namespace App\Context\Product\Events;
use App\Context\Product\Models\Product;
class ProductCreatedEvent{ public Product $product;
public function __construct(Product $product) { $this->product = $product; }}
复制代码


如你所见,这里没有什么特别的事情。我们只是将模型用作事件属性。


事件溯源部分

现在,我们转到应用的 Order 部分,该部分是事件溯源的。它的结构如下所示:



你会在此处看到 projector 和聚合根,因此很明显,这部分使用了事件溯源。我们正在使用我们自己的事件溯源包来处理有关存储事件、projector、聚合根……的逻辑。


在应用的 Product 部分,我们触发了事件。这些事件只是常规的 Laravel 事件,我们没有存储它们。ProductEventSubscriber正在侦听这些事件。看一下代码。


namespace App\Context\Order\Subscribers;
use App\Context\Product\Events\DeletingProductEvent as AdminDeletingProductEvent;use App\Context\Product\Events\ProductCreatedEvent as AdminProductCreatedEvent;use App\Context\Order\Events\ProductCreatedEvent;use App\Context\Order\Events\ProductDeletedEvent;use App\Support\Events\EventSubscriber as BaseEventSubscriber;use App\Support\Events\SubscribesToEvents;
class ProductEventSubscriber implements BaseEventSubscriber{ use SubscribesToEvents;
protected array $handlesEvents = [ AdminProductCreatedEvent::class => 'onProductCreated', AdminDeletingProductEvent::class => 'onDeletingProduct', ];
public function onProductCreated(AdminProductCreatedEvent $event): void { $event = $event->product;
event(new ProductCreatedEvent( $event->getUuid(), $event->stock, )); }
public function onDeletingProduct(AdminDeletingProductEvent $event): void { $event = $event->product;
event(new ProductDeletedEvent( $event->getUuid(), )); }}
复制代码


这里发生的事情很有趣。我们将监听来自非事件溯源部分的事件。当来自 Product 上下文的事件(对于 Order 上下文感兴趣)进入时,我们将触发另一个事件。另一个事件是 Product 上下文的一部分。


因此,当出现App\Context\Product\Events\ProductCreatedEvent(且别名为AdminProductCreatedEvent,因为可能是管理员在 UI 上执行的操作导致该操作)时,我们将触发App\Context\Order\Events\ProductCreatedEvent(来自 Order 上下文)。


我们将获取对 Order 上下文感兴趣的所有属性,并将它们放在App\Context\Order\Events\ProductCreatedEvent中。


public function onProductCreated(AdminProductCreatedEvent $event): void{    $event = $event->product;
event(new ProductCreatedEvent( $event->getUuid(), $event->stock, ));}
复制代码


看一下这个事件本身。


namespace App\Context\Order\Events;
use App\Support\ValueObjects\ProductUuid;use Spatie\EventSourcing\ShouldBeStored;
class ProductCreatedEvent extends ShouldBeStored{ public ProductUuid $productUuid;
public int $stock;
public function __construct(ProductUuid $productUuid, int $stock) { $this->productUuid = $productUuid;
$this->stock = $stock; }}
复制代码


你可以看到这个事件扩展了ShouldBeStored。这个基类是我们的事件溯源包的一部分。这将导致该事件被存储下来。


我们立即使用这个存储的事件来建立一个包含存货(Stock)的 projection。看一下ProductStockProjector


namespace App\Context\Order\Projectors;
use App\Context\Order\Events\OrderCancelledEvent;use App\Context\Order\Events\ProductCreatedEvent;use App\Context\Order\Events\ProductDeletedEvent;use App\Context\Order\Events\OrderCreatedEvent;use App\Context\Order\Models\Order;use App\Context\Order\Models\ProductStock;use App\Support\ValueObjects\ProductStockUuid;use Spatie\EventSourcing\Projectors\Projector;use Spatie\EventSourcing\Projectors\ProjectsEvents;
class ProductStockProjector implements Projector{ use ProjectsEvents;
protected array $handlesEvents = [ ProductCreatedEvent::class => 'onProductCreated', ProductDeletedEvent::class => 'onProductDeleted', OrderCreatedEvent::class => 'onOrderCreated', OrderCancelledEvent::class => 'onOrderCancelled', ];
public function onProductCreated(ProductCreatedEvent $event): void { ProductStock::create([ 'uuid' => ProductStockUuid::create(), 'product_uuid' => $event->productUuid, 'stock' => $event->stock, ]); }
public function onProductDeleted(ProductDeletedEvent $event): void { ProductStock::forProduct($event->productUuid)->delete(); }
public function onOrderCreated(OrderCreatedEvent $event): void { $productStock = ProductStock::forProduct($event->productUuid);
$productStock->update([ 'stock' => $productStock->stock - $event->quantity, ]); }
public function onOrderCancelled(OrderCancelledEvent $event): void { $order = Order::findByUuid($event->aggregateRootUuid());
$productStock = ProductStock::forProduct($order->product->uuid);
$productStock->update([ 'stock' => $productStock->stock + $order->quantity, ]); }}
复制代码


触发ProductCreatedEvent时,我们将调用ProductStock::createProductStock是常规的 Eloquent 模型。


namespace App\Context\Order\Models;
use App\Context\Product\Models\Product;use Illuminate\Database\Eloquent\Model;
class ProductStock extends Model{ /** * @param \App\Context\Product\Models\Product|\App\Support\ValueObjects\ProductUuid $productUuid * * @return \App\Context\Order\Models\ProductStock */ public static function forProduct($productUuid): ProductStock { if ($productUuid instanceof Product) { $productUuid = $productUuid->getUuid(); }
return static::query() ->where('product_uuid', $productUuid) ->first(); }}
复制代码


ProductStockProjector中,当收到订单、取消订单或删除订单时,将更新ProductStock模型。


现在,我们与参与该项目的每位成员都达成一项协议,那就是不允许从其他上下文写入模型。Order 上下文可以从这个模型写入和读取,而 Product 上下文只能读取它。由于这是一个重要的规则,因此我们可能会很快用代码把它固定下来。


最后,我们看一下使用ProductStock模型进行决策的OrderAggregateRoot


namespace App\Context\Order;
use App\Context\Product\Models\Product;use App\Context\Order\Events\CouldNotCreateOrderBecauseInsufficientStock;use App\Context\Order\Events\OrderCreatedEvent;use App\Context\Order\Events\OrderCancelledEvent;use App\Context\Order\Exceptions\CannotCreateOrderBecauseInsufficientStock;use App\Context\Order\Models\ProductStock;use Spatie\EventSourcing\AggregateRoot;
class OrderAggregateRoot extends AggregateRoot{ public function createOrder(Product $product, int $quantity): self { $eventAvailability = ProductStock::forProduct($product);
if ($eventAvailability->availability < $quantity) { $this->recordThat(new CouldNotCreateOrderBecauseInsufficientStock( $product->getUuid(), $quantity ));
throw CannotCreateOrderBecauseInsufficientStock::make(); }
$unitPrice = $product->unit_price;
$totalPrice = $unitPrice * $quantity;
$this->recordThat(new OrderCreatedEvent( $product->getUuid(), $quantity, $unitPrice, $totalPrice, ));
return $this; }
public function cancelOrder(): self { $this->recordThat(new OrderCancelledEvent());
return $this; }}
复制代码


在一个完整的应用中,createOrder函数可能由 UI 中执行的操作触发。产品可以被订购一定的数量。使用ProductStock,聚合根将决定是否有足够的库存来创建订单。


小结

应用的非事件溯源部分会触发常规事件。事件溯源部分会监听这些事件,并在它们自己的事件中记录它们感兴趣的数据。这些属于上下文的事件将被存储下来。这些存储的事件将提供给 projector。这些 projector 创建的模型可以由非事件溯源的部分读取。非事件溯源的部分只能读取这些模型,不能写入。聚合根可以使用事件和 projection 来做出决策。


这种方法可以带来很多好处:


  • 可以像我们过去那样构建非事件溯源的部分。我们只需要注意被触发的事件即可

  • 事件溯源部分可以记录自己的事件以采取行动

  • 我们可以创建新的 projector 并重播所有记录的事件,以建立新状态


我知道这些要吸收起来没那么容易。这种方法并不是一成不变的。我们尚处于探索阶段,但我们对于已经掌握的部分信心十足。


英文原文:


Mixing event sourcing in a traditional Laravel app


2020-06-02 16:461270
用户头像
王强 技术是文明进步的力量

发布了 734 篇内容, 共 297.4 次阅读, 收获喜欢 1624 次。

关注

评论

发布
暂无评论
发现更多内容

Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合

比伯

Java 架构 面试 编程语言 计算机

第十周 模块分解 总结

三板斧

极客大学架构师训练营

淘宝直播技术干货:高清、低延时的实时视频直播技术解密

JackJiang

音视频 即时通讯 视频编码 直播技术

使用sonar扫描svn中的代码后,没有作者或责任人信息

lee

svn 代码质量 sonar

架构师训练营 -week10-作业

大刘

极客大学架构师训练营

原理实践,全面讲解Logstash+Kibana+kafka

996小迁

Java 程序员 架构 面试

一口气看完45个寄存器,CPU核心技术大揭秘

程序员架构进阶

cpu 操作系统 寄存器 核心

私域流量运营03|衡量企业运营视频号的4个关键指标

Linkflow

客户数据平台 客户画像 视频号

美妆行业:低代码全域客户数据采集,赋能数据化运营

Linkflow

营销数字化 客户数据平台 CDP

很简单却能让你面试头疼得Java容器,这里从源码给你解释清楚

小Q

Java 学习 源码 容器 面试

谁说产品经理和程序员之间不能和平共处?

华为云开发者联盟

DevOps 产品经理 用户地图

区块链赋能保险理赔,宁波开启“零感知理赔”试点

CECBC

区块链 保险理赔

Linux笔记(一):基本命令

Leo

Linux 大前端 笔记

BitArray虽好,但请不要滥用,一次线上内存暴增排查

AI乔治

Java 架构 JVM 内存泄露

性能优化:线程资源回收

AI乔治

Java 架构 JVM 性能调优

用FL Studio基础版制作一首完整的电音

奈奈的杂社

音乐制作 编曲 电音 电音制作 中国电音

LeetCode题解:17. 电话号码的字母组合,回溯,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

从微服务应用于技术栈,了解华为云微服务应用

华为云开发者联盟

微服务 服务 云技术

浅谈原子操作

阿里云基础软件团队

内核

架构师训练营 1 期 - 第十周 - 模块分解

三板斧

极客大学架构师训练营

【2020GET】即构科技蒋宁波:教育行业客户需求的核心是什么?

ZEGO即构

区块链溯源有哪些优势?区块链产品溯源系统搭建

13530558032

K8S CSI容器存储接口(一):介绍以及原理

silenceper

Kubernetes CSI

区块链版权应用开发,区块链助力版权保护

13530558032

实体经济的数智化要塞,为什么是供应链?

脑极体

架构师训练营第十周作业

我是谁

极客大学架构师训练营

服务器选择要注意什么?

德胜网络-阳

全球至少有36家央行发布了央行数字货币计划

CECBC

数字货币

K8S CSI 容器存储接口 (二):如何编写一个CSI插件

silenceper

Kubernetes Kubernetes源码 CSI

当艺术品遇上区块链:金丝楠木艺术品溯源

CECBC

区块链 溯源 艺术品

一文搞懂所有HashMap面试题

编程 面试 计算机

如何设计一款大型Laravel应用程序的架构(下)?_文化 & 方法_Freek_InfoQ精选文章