网络知识 娱乐 大道至简 | 了解代码中耦合

大道至简 | 了解代码中耦合

了解代码中耦合

我们经常说耦合,耦合到底是什么?

大道至简

通常,存在三种类型的组件耦合。

  1. 传入耦合:A 组件的任务必须依赖于 B、C 和 D 的实现。
大道至简

2、传出耦合:A组件的任务完成后,必须执行B、C、D。

大道至简

3、时序耦合:A组件的任务完成后,必须执行B和C。另外,B早于C。

大道至简

这里提到的组件可以是源代码级别、模块级别甚至是基于粒度的服务级别。

在本文中,我们将特别深入探讨时序耦合,因为这是最常见和最容易被忽视的陷阱。首先我们在 Node.js 中描述如下:

大道至简

至此,我们发现这确实是通用的。我们几乎所有的代码都是这样的。在一个方法中依次做三件事是很正常的,不是吗?

让我们举一个更具体的例子。假设我们有一个具有功能的电子商务,purchase。因此,我们开始以一种简单的方式进行编码。

大道至简

首先总结一下购物车中所有商品的价格。然后调用支付服务处理信用卡。很简单,对吧?

好的,营销团队想让消费超过1000美元的人获得优惠券,所以我们继续修改我们的purchase.

大道至简

这个功能也很常见,后来销售团队发现优惠券是一种很好的促销方式,于是提出达到5000美元的人可以获得抽奖机会。这purchase一直在增长。

大道至简

这是一个时序耦合。不管是依赖giveCoupon还是lottery实际依赖purchase,都必须在生命周期内完成purchase。一旦特征需求越来越大,整体的性能purchase就会被不断拖累。尤其是lottery通常需要巨大的计算,并且purchase被迫等待lottery成功被认为是成功。

通过域事件解耦时序

从上一节我们了解到,purchase应该只需要处理支付,其余的行为是附加的。换句话说,即使giveCoupon失败,也不应该影响purchaseor lottery。

领域驱动开发中有一种方法称为领域事件。当一个任务完成时,它会发出一个事件,关心该事件的处理程序在收到该事件后可以采取相应的动作。顺便说一下,这种方法在设计模式中也被称为观察者模式。在领域驱动开发中,“通知”包含领域的通用语言,因此该通知被命名为领域事件。

因此,让我们purchase以Node的方式稍微修改一下。

大道至简

通过事件,我们可以完全解耦giveCoupon并lottery从purchase. 即使任何一个处理程序发生故障,也不会影响原始支付流程。

而purchase只需要专注于支付过程。支付成功后,发出事件,让其他函数接管。

大道至简

如果以后有更多的需求,原来的就不用改了purchase,只需要增加一个新的handler即可。这就是解耦的概念。这里我们去掉了代码级耦合和时序级耦合。

如何处理事件丢失

无论何时发生故障,我们都必须期待它们并优雅地处理它们,这样可以称为弹性工程

当我们通过领域事件将优惠券和彩票解耦时,我们马上就会面临一个问题。如果事件丢失了怎么办?付款完成了,但是优惠券还没有发出,这对客户来说绝对是个大问题。

换句话说,我们如何确保发出的事件会被执行。这正是将消息队列引入系统的原因

我们之前讨论过消息队列,在消息传递中存在三个不同级别的保证,分别是:

  • 最多一次
  • 至少一次
  • 恰好一次

大多数消息队列都有至少一次保证。也就是说,通过消息队列我们可以保证所有的事件至少执行一次。这也确保了消息不会丢失。

因此,为避免事件丢失,我们将更改为emitter.emit使用 RabbitMQ 或 Kafka 之类的队列提交。在这个阶段,我们在系统层面引入了解耦,即让事件生产者和消费者属于不同的执行单元

如何处理事件丢失

我们已经可以确保执行发出的事件。如果事件根本没有发送怎么办?继续purchase举例,whenpayByCreditCard已经成功,但是由于意外原因导致系统崩溃而没有发送事件。然后,即使使用消息队列,我们​仍然得到不正确的结果。

为了避免这个问题,我们可以利用事件溯源。在分布式事务CQRS中,已经描述了事件溯源的核心概念。

在发出事件之前,先将事件存储到存储中。处理程序完成事件处理后,将存储中的事件标记为“已处理”。

需要注意一件事,事件的编写和付款必须在同一笔交易下。这样,只要支付成功,事件也会写入成功。最后,我们可以定期监控过期事件以了解问题所在。

结论

这一次,我们仍然像我们在从单体应用到 CQRS中所做的那样,逐步进行系统演进,让你知道当系统变得庞大和复杂时如何解耦。一开始,我们首先通过领域事件将源码和执行时序解耦;然后我们引入了消息队列与消息的生产者和消费者,实现了系统级的解耦。

正如我之前所说,一个系统的进化是为了解决一个问题,但它也会产生新的问题。我们只能选择最可接受的解决方案,并在复杂性、性能、生产力和其他因素上寻求妥协。

将一个完整的动作拆分成不同的执行单元必然会遇到不一致。在解决不一致时,有许多考虑因素,例如:

  • 不管事件是否会丢失,只要使用最简单的架构,EventEmitter这种方式是最简单的,80%的情况下可能没有问题,但是如果出现问题了怎么办呢?
  • 尽量做到可靠,所以引入消息队列,应该有 99% 的把握不会有问题,但是还有1%,这样的风险可以承受吗?
  • 实施事件溯源是以增加复杂性为代价的,并且性能可能会受到影响。这可以接受吗?

就像我常说的,系统设计没有完美的解决方案。每个组织都有不同程度的风险承受能力。在各项指标中,我们为自己寻找最能接受的解决方案,随时思考我们面临的风险和失败。因此,每个人都应该能够建立一个有弹性的系统。