事件是一等公民

在面向对象编程中,对象是一等公民,后来在函数式编程中,函数是一等公民,而如今在事件驱动编程中,事件是第一公民,事件其实是对象与函数的集合体,能够利用两者优点,回避缺点。

这篇文章介绍著名的服装电子商务企业Stitch Fix是如何使用事件驱动实现软件系统建设的,Stitch Fix曾经两次出现在互联网女皇Mary Meeker的《互联网趋势报告》上,不同于传统电商其实是一个大仓库,用户从中挑选商品,Stitch Fix并不需要你亲自去挑选任何商品,而是根据约定的周期为你邮寄一盒商品(她们称之为一个“Fix”),你可以从中选择自己满意的商品留下。如果这些商品你都不中意,那你可以免费退回给Stitch Fix。

大意如下:

在这篇文章中,我将讨论Stitch Fix(服装电子商务)实现的全过程,以及我们如何使用事件对它进行建模。我建议我们应该把事件看作是我们系统中的一流公民,因为它们帮助我们把系统的某些部分实现了分离,并且独立地推理它们。最后,我将讨论在现实世界中使用事件有多么普遍,并用软件开发本身的隐喻来帮助基于事件的直觉开发。

一个Fix实现的全过程
在Stitch Fix,工程团队建立并运行了70多个独立的应用程序和服务,服务于Stitch Fix业务的各个方面。我们有负责采购服装的商品销售团队,仓储运输团队以及我们的成千上万的造型团队为我们的客户选择它们。我们的客户会使用这些应用程序来安排他们的业务,进行评分和支付。有专门供客户体验团队使用的应用程序来帮助我们的客户提供最好的服装购买体验。

当一个客户告诉我们她想在特定的日子接收约定的商品时,就会创建一个新的Fix程序。根据何时何地运送Fix商品,我们将其分配到我们在美国的一个仓库(仓库分配),然后由我们的3500名造型师(设计师)指定样式。设计师选择她期望客户享受(样式)的5个项目,并且准备发送它。仓库工作人员挑选,打包并运送Fix商品(包括运输),客户在她家门口就会收到。她留下自己想要的商品,并返回她不需要(买单)的商品。整个Fix周期完成。

我们刚刚描述了一个适度复杂的工作流程,其中有许多单独的步骤,都在一个Fix流程中完成。从软件工程的角度来看,模型化的直接方式就是建立一个状态机,带有一些特点的事件,这些事件表达Fix流程已经从一个状态转换到另一个状态。而这正是我们实现它的方式。下面是这个工作流程的一个简化的表示:

请求一个Fix
- >Fix是_scheduled(状态/事件)

分配Fix给仓库
- >Fix是_hizzy_assigned(仓库名称就叫“hizzies”;不要问为什么)

将Fix分配给设计师
- >Fix是_stylist_assigned

Fix中商品设计完成
- >Fix是_styled

为当前Fix挑选商品条目
- >Fix是_picked

将这些物品打包装入一个盒子里
- >Fix是_packed

通过运输公司运送
- >Fix是_shipped

Fix商品到达客户端
- >Fix是_delivered

客户决定要留下想要的和返回不要的,为留下的支付买单
- >Fix是_checked_out

下面几件事可考虑:

1. 随着Fix流程快乐地移动,不同的应用程序或服务通过更多(元)数据来丰富它,将其与其他东西连接起来,或者通过对物品或包装物理做一些事情来做些事情。这样做的结果将Fix从一个状态到另一个(状态转换)。

2. 下一个应用程序或服务只能在前一个应用程序或服务完成时才能完成,因此需要知道(事件)是何时进行的。

3. 我们不能跳过任何这些步骤,否则会丢失一些事情 - 在许多情况下,确实是这样。换句话说,从一个给定的状态出发,只能进入状态机的中其他状态的一个子集。

4. 如果我们只记得Fix当前的状态 ,我们会错过很多事情。我们希望能够询问现在Fix流程到哪儿了,但是我们也想知道它曾经到过哪里,在那里呆了多久,以及是何时移到下一步的。所以如果我们只将它的当前状态存储在某个数据库表中,而没有其他内容,我们会在这些问题上被卡住。相反,我们还是需要记录下所有Fix流程的步骤。

事件作为一等公民

请注意,如果我们只有经典的三层架构的标准工具可供使用,那么我们会遇到麻烦。我们都熟悉三层架构如下:

1.表现层:用户与系统交互的用户界面
2.应用层:我们通常无状态地工作的“业务逻辑”
3.持久层:我们存储数据的地方,通常在数据库中

我坚信事件代表了第四个基本要素:

4.事件:表达发生了一个有趣的事情,或根据维基百科意味着“状态发生了重大变化 ”。

在像Stitch Fix这样的微服务架构中,某个特定的应用程序或服务可能是事件的生产者、事件的消费者或两者兼而有之。例如侦听_stylist_assigned事件的服务,它会显示设计师需要的所有信息以供其修改样式风格。当她完成Fix一个工序以后,她点击“发送”按钮,那么这个服务或应用程序将发布_styled事件。仓库部门的服务将倾听该事件,并可以开始下一道工作等。

消费这些事件并产生这些事件是这些应用程序和服务的头等重要大事; 他们需要这些事件来完成他们的工作。所以当我们用“接口”这个名词谈论这些服务时,让我们来明确一下。一个服务接口包括:

1.同步的请求 - 响应操作(例如,使用REST / JSON数据格式,但它可以很容易地通过gRPC,Thrift等通讯)
2.服务产生的事件
3.服务消耗的事件
4.批量读取和写入(例如,将服务中的数据提取到分析系统中的ETL)

更一般地说,一个服务的接口包括获取服务的数据进出的任何机制。包括作为作为服务所有者或服务消费者,有时我们将自己忘记了。

事件实现解耦

一个事件的生产者发布事件,零或多个的消费者订阅它。也许没有人在倾听; 也许只有一个; 也许很多。生产者不知道也不关心。这给了生产者和消费者完全相互分离的好处。我们可以添加更多的消费者,删除他们,或者扩大或缩小他们 - 因为没有生产者是聪明得能够提前预知有多少其他服务会使用它的结果(banq:过度设计与理性有限性)。

事件作为记录

一旦我们将实体的所有有趣的状态转换表示为事件,我们就可以使用这些事件来记录该实体发生了什么以及在何时发生了什么。当我们想要回头看看发生了什么时,这些记录都是非常有价值的。我们的客户体验团队(也就是“客户支持”团队,但有更多的微笑和同理心),他们试图查看一个客户的Fix的历史流程步骤是很常见的。我们的数据科学团队通常会使用Fix程序中的事件来优化我们工作流程等各个方面。我们的工程团队通常使用历史事件作为调试和诊断工具。

因此,如果我们只保留事件(banq注:以事件替代状态,因为状态和事件是同一个意思,事件发生意味状态改变,状态改变也意味着有事件发生了),而不是在数据库持久存储当前的状态。毕竟,我们总是可以简单地通过播放事件来重建当前状态。这是一个非常聪明的想法,人们已经想到了 - 它被称为“事件溯源EventSourcing”(参见 Greg Young, Chris Richardson的许多着作),并且它有很多美妙的弹性特性,特别是在分布式系统。事实上,有基于这个想法的整个软件系统(Event Store,Kafka,Akka Persistence等)。当然,在会计中双项记账也是如此,所以即使这些(非常聪明的)人都被700年前的美第奇家族的女人抢走了(banq注:聪明男人被控制欧洲的家族女人抢了)。

事件是真实世界的工作方式

我经常听说开发者很难从事件的角度思考。如果你习惯于构建经典的三层架构,那么可能会感觉有些不合常理。主要是你没有敏感到:在任何时候在我们的系统的某个部分可能发生了什么事情,这种行为发生的影响对我们系统的其他部分还够不明显。我们在这里使用“最终一致性”和“不同步”等词语,它们是因为难以推理而赢得声誉。但是我认为你对事件的直觉比你想像的要多得多。如果你可以把你的问题看成是一个工作流程,那么你已经完成了一半以上的工作。(banq注:转换思路,有人工介入的电脑程序都是流程)

所以我们举一个例子,这个例子对于每个软件工程师来说都是熟悉的SVN, 想象一下,一个典型的现代软件开发过程,我们编写代码,上传它到源代码控制,测试,分级和部署。我们经常把这作为一个“生命周期”或“管道”或“工作流程”来讨论。那听起来很像事件。让我们来看看。
编写代码
- >代码是_submitted

测试代码
- >代码是_tested

将其部署到测试服务器
- >代码是_staged

部署到生产服务器
- >代码是_deployed

这似乎超级熟悉 - 我们每天都这样做!这不仅仅是一个切入点,对于我们大多数人来说,这是我们的工作。

想想看,如果这其实是一个异步工作流程,但是我们使用同步方式去强制实现了(banq注:不巧妙,笨重)。想象一下,每当你在IDE中点击确认,你的代码都会被部署到生产环境中(这就是使用同步的下场)。这将是很疯狂的。

你不能因为系统的一个部分与另一个部分之间存在“不一致”而认为这个想法有点愚蠢。在一天的大部分时间里,你的代码在你的笔记本电脑上都各自有一个版本,而在生产中有另一个“陈旧”的版本,这一切都是正常地在发生。

Events As First-Class Citizens – Hacker Noon