2020 Google开发者大会重磅开幕 了解详情

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

2020 年 6 月 02 日

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


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


首先来看场景概况。这是我们曾经做过的一个比较大的项目。一旦项目完成,它将为数以十万计的用户提供服务,处理大量财务交易,并且需要即时创建独立的租户专属安装。该项目的一个关键需求是需要轻松地报告和追踪产品订购流程的历史记录,这一流程也是业务的核心。


除了这个面向前端的客户流程外,还会有一个复杂的管理面板来管理产品。这里几乎完全不需要报告或追踪管理活动的历史记录。主要目标是提供一个易于使用的产品管理系统。


希望你能理解我故意使用模糊术语的做法,因为这显然不是一个开源项目。不过,我认为“产品管理”和“订单”的概念足以让你了解我们所做的是怎样的设计决策了。


我们首先来讨论这个系统的一种设计方法,这种方法出自我之前写过的《超越CRUD的Laravel》文章系列。


在这样的系统中可能会有两个域组:Product 和 Order,以及两个同时使用这些域的应用程序:AdminApplicationCustomerApplication


简化版本如下所示:



在之前的项目中,我们成功应用了这一架构,所以这次也可以直接使用它了事。但它也有一些缺点,特别是对于这个新项目而言,缺陷很突出:我们必须牢记,报告和历史记录跟踪是订购流程的关键之处。我们希望能在代码中体现这一点,而不是简单地加个副效果就行。


例如:我们可以使用活动日志包来跟踪订单过程中出现的“历史消息”。我们还能在订单和历史记录表上编写自定义查询以生成报告。


但是,这些解决方案只能成为核心业务的简单的副效果,否则就没法正常工作。可是我们的情况下并没有这个条件。因此,Freek 和我的任务就是为这个项目设计一个方案,让报告和历史记录跟踪成为应用程序中易于维护和易于使用的核心部分。


很自然地,我们将目光投向了事件溯源,这是一种可以满足上述要求的优秀而灵活的解决方案。但没有什么是免费的午餐:事件溯源需要编写很多额外代码才能完成其他方法下很简单的事情。在有些地方,你过去只需要简单的 CRUD 操作处理数据库中的数据即可,可现在你不得不操心事件调度,还要用 projector 和 reactor 处理事件,同时还要一直关注版本控制。


很明显,事件溯源系统能解决许多问题;但即使在一些没有任何收益的地方,它也会带来很多开销。


这就是我的意思:如果我们决定事件溯源 Orders 模块(它依赖于 Products 模块中的数据),那么我们还需要事件溯源后者,否则的话我们可能会撞上无效状态。如果 Products 没有事件溯源,并且已被删除,则我们将无法重建 Orders 状态,因为它缺少信息。


因此,要么我们事件溯源一切内容,要么就设法解决这个矛盾。


需要事件溯源一切内容吗?


过去,在一些业余项目中使用事件溯源的经历让我们痛苦地意识到,我们不应该低估它所增加的复杂性。此外,Greg Young 表示事件溯源整个系统往往不是一个好主意——他对人们关于事件溯源的误解有完整的论述,值得一看!


很显然,我们不想事件溯源整个应用程序。这样做根本没有任何意义。唯一的选择是找到一种将有状态系统与事件溯源系统结合在一起的方法,但可惜在这个主题上我们发现没什么资源可用。


不管怎样,我们还是进行了一些费工费力的研究,并设法找到了问题的答案。但这个答案不是来自事件溯源社区,而是来自一种完善的 DDD 实践:限界上下文。


如果我们希望 Products 模块成为一个独立的,有状态的系统,则必须清晰地尊重 Products 和 Orders 之间的界限。我们不能把这两个模块视为一个单体应用程序,而必须将它们视为两个单独的上下文——也就是单独的服务,两者之间通信时必须确保 Order 上下文永远不会进入无效状态。


如果构建的 Order 上下文不直接依赖于 Product 上下文,那么这个 Product 上下文是怎么构建的就无关紧要了。


在与 Freek 讨论时,我提到:将 Products 视为可通过一个 REST API 访问的独立服务。当 API 下线或改动了自己的数据结构时,我们如何保证事件溯源应用程序仍然可以正常工作呢。


显然,我们实际上并不会构建在服务之间通信的 API,因为它们将位于同一服务器上的同一代码库中。但在设计系统时有这样的思考还是很好的。


边界是下面这个样子,其中每个服务都有自己的内部设计。



如果你读过了我的《超越 CRUD 的 Laravel》系列文章,那么肯定已经熟悉了 Product 上下文的工作机制。那边就没什么新东西要讲了。不过 Order 上下文可以多讲一些背景信息。


事件溯源一部分内容


下面我们看一下事件溯源的部分内容。我假设你之所以会阅读这篇文章,至少是因为你对事件溯源感兴趣,所以我不会详细解释所有内容。


OrderAggregateRoot将跟踪在这一上下文中发生的所有事件,并将成为与应用程序对话的入口点。它还将调度事件,事件被存储并传播到所有 reactor 和 projector。


Reactor 将处理副效果,而这些副效果将永远不会重放,并且 projector 将进行投影。在我们的项目中,这些都是简单的 Laravel 模型。尽管只能从 projector 内部写入这些模型,但可以从其他任何上下文中读取它们。



我们在这里做出的一个设计决策是不拆分读写模型,因为现在我们使用了口头和书面约定,要求这些模型只能通过它们的 projector 写入。这种投影模型的一个例子就是一个 Order。


要记住的一条最重要规则是,只能从 Order 存储的事件中重建 Order 上下文的整个状态。


那么我们如何从其他上下文中提取数据呢?当与 Product 相关的上下文中发生某些事情时,如何通知 Order 上下文?可以肯定的是:与 Products 有关的所有信息将需要作为事件存储在 Order 上下文中;因为在这一上下文中,事件是唯一的真实来源。


为了做到这一点,我们引入第三种事件监听器。我们已经有了 projectors 和 reactors;现在我们添加订阅者(subscribers)的概念。允许这些订阅者侦听来自其他上下文的事件,并在其当前上下文中进行相应的处理。看起来,它们几乎总是将外部事件转换为内部存储事件。



从事件存储在 Order 上下文中的那一刻起,我们就可以放心地忘记对 Product 上下文的任何依赖。


有些读者可能认为我们会通过在这两个上下文之间复制事件来复制数据。当然,我们将基于 Product 的created时间存储特定于 Orders 的事件,因此的确会复制一些数据。但是,这样做带来的好处比你想象的更多。


首先:Product 上下文不需要知道其他哪些上下文将使用它的数据。它不必考虑事件版本控制,因为它的事件永远都不会被存储。这样我们就可以在处理 Product 上下文时将它视为正常的有状态应用程序,无需加入事件溯源,也就避免其复杂性。


第二:会被事件溯源的不只是 Order 上下文,而且所有这些上下文都能单独侦听 Product 上下文中触发的相关事件。


第三:我们不必存储原始 Product 事件的完整副本,因为每个上下文都可以挑选和存储与自己用例相关的数据。


数据迁移问题呢?


一个新问题出现了。


假设这套系统已经投入生产一年之久,我们决定添加一个新的事件溯源上下文;它还需要有关 Product 上下文的信息。由于上面列出的原因,原始的 Product 事件没有被存储下来——那么我们如何为新的上下文建立初始状态?


答案是这样的:在部署时,我们必须读取所有产品数据,并根据现有产品将相关事件发送到新添加的上下文中。这种一次性迁移的麻烦是额外的成本,但它让我们可以自由地处理 Product 上下文,而不必担心外部环境。对于这个项目,这是值得付出的代价。


最终整合


最后,通过使用只读模型,我们就能使用从所有上下文收集的应用程序数据。到目前为止,我们的约定依旧要求这些模型是只读的;而将来可能会改变这个约定。



从应用程序到 Product 上下文,就像普通的有状态应用程序一样通信即可。应用程序和事件溯源的上下文(例如 Orders)之间是通过其聚合根通信的。


下面是最终成品的概述。这张图中还缺少一些箭头,但是上下文和应用程序之间与它们内部的相关流程画的应该足够清楚了。



解决我们问题的关键来自 DDD 的限界上下文思想。它们描述了我们代码库中的严格界限,我们不能随意跨越这些界限。当然这增加了一层复杂性,但它还让我们能够自由地以期望的方式构建每个上下文,而不必操心支持其他上下文的问题。


最后一个难题是仅依靠事件作为上下文之间的交流手段。它又增加了一层复杂性,但同时也是一种解耦和增加灵活性的方式。


第二部分则是讲述我们如何在一个 Laravel 项目中编写具体的代码。


英文原文:


Combining event sourcing and stateful systems


2020 年 6 月 02 日 14:27 769
用户头像

发布了 454 篇内容, 共 168.0 次阅读, 收获喜欢 952 次。

关注

评论

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

做产品的同理心

孙苏勇

产品 产品经理 产品设计

禁止在构造函数里调用虚函数

喵叔

C# .net 编码习惯

如何做一名失败的安全架构师

石君

架构 安全架构师 安全评估

喔,明白了,成功也是一种苦难

泰稳@极客邦科技

创业 身心健康 企业文化 个人成长 心理

是时候要说再见了,春风十里,不如邮你!

乐少

精纯还是混乱?职场十二箴言——重读“成为乔布斯”的思考(一)

石君

职场 乔布斯 成功学

小议RPA

一品凡心

人工智能 RPA 自动化

任正非管理哲学中的三个常识和三种科学

泰稳@极客邦科技

创业 团队管理 华为

规范约束条件

喵叔

C# .net 编码习惯

减少装箱与拆箱

喵叔

C# .net 编码习惯

1. 什么是Xamarin.md

喵叔

C#

走出舒适区最好办法别走了,扩大它

乐少

删掉最后一句话

池建强

心理学 情绪控制

多用as少用强制类型转换

喵叔

Elasticsearch文档版本冲突原理与解决

Skysper

elasticsearch 乐观锁 悲观锁

翻译: Effective Go (2)

申屠鹏会

go 翻译

初入响应式编程(上)

CD826

spring 微服务 Spring Cloud 响应式编程 reactor

精纯还是混乱?职场十二箴言——重读“成为乔布斯”的思考(二)

石君

创业 乔布斯 成为乔布斯

《小狗钱钱》——财富离我们并不遥远

尹晓铁

读书笔记 投资 成长 思维方式

此为开卷

范学雷

【深度】为您解读东西方艺术教育的专业设置差异对比~

默聲

做小池塘里的大鱼,还是大池塘里的小鱼?这是个问题。

泰稳@极客邦科技

创业 团队管理 目标管理

特别评论:甲骨文的傲气

张晓楠

云计算 互联网巨头 企业文化

测试

Chonge

HTTP Methods和RESTful API的设计

孙苏勇

架构 系统设计 RESTful 接口

分布式数据库是无用的屠龙术吗?

海边的Ivan

企业架构 分布式数据库 业务中台

2.Hello Xamarin

喵叔

C#

从流程、认知上做稳定的系统演进

Skysper

系统设计 质量管理

var lady first

喵叔

C# .net 编码习惯

浅谈汽车行业嵌入式软件发布的流程有多复杂

WB

程序员 软件

dubbo-go 中如何实现远程配置管理

joe

golang Apache 开源 微服务架构 dubbo

2020中国技术力量年度榜单盛典

2020中国技术力量年度榜单盛典

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