点击左上角,关注:“锅外的大佬”

专注分享国外最新技术内容,帮助每一个技术人更优秀地成长

1.介绍

这是在架构级应用 SOLID原则的系列文章的第一篇。如果你熟悉 OOP中的类设计的 SOLID原则,如果你想知道在设计系统架构的时候是否可以使用他们,我将尝试给你一些见解。
在类的级别,开闭原则( Open-Closed-PrincipleOCP)表示一个类对扩展开放但对修改关闭,这意味着能在不修改类的情况下扩展类的行为。一般通过继承和组合扩展类来完成。
在架构级别,我们不尝试修改系统的一部分(最适合你架构的进程、守护程序、服务或微服务)的功能,而是利用你已经完成的工作添加新的部分。为了不修改现有部分,系统需要完全解耦。我将重点介绍事件驱动型系统,其中服务通过消息队列进行通信。这可以是 ActiveMQRabbitMQZeroMQKafka或任何其他服务,但我将使用 Kafka的术语,例如主题(Topic)、发布者(publisher)和订阅者(subscriber),以及 Kafka能让相同的主题有不同的订阅者。

2.消息系统

在上图中可以看到普通的用例:发布者发送消息(事件)到主题,然后多个订阅者可以从主题中获取事件。箭头展示了通信流。假设发布者和订阅者都是微服务,双圆角矩形表示特定微服务的多个实例。在这种情况下,我们有四个微服务: 发布者订阅者1订阅者2订阅者n,每一个都有多个实例。

3.举例说明

想象我们在汽车租赁公司工作,负责建立一个新的车辆可用数系统。这个租赁流程简化视图如下:
在租车过程中,签订租赁协议,客户取走汽车。汽车的可用数减少一。然后是客户使用车辆的时间(租赁时间),最后,我们必须考虑汽车的的归还和汽车的登记。发生这种情况时,汽车的可用数增加1。在汽车租赁和汽车登记这两种情况中,我们保存一份租赁协议到数据库,因此我们可以定义一个事件,
RentalAgreementSaved
,这将在保存数据之后被触发。该事件将被存储在
RentalAgreementSaved
主题中。目前,我们有两个发布者发送消息到主题,微服务
CarRental
CarCheckin

现在,我们需要定义消息的内容。由于主题的目的是表达一个租赁协议已被保存,因此我们需要的最少量的信息是协议ID。但该系统的主要目的是追踪车辆的可用性,因此最好有一个 Status字段来帮助我们。该字段可以有两种可能的值:
  • Active: 代表车辆被另一个客户使用
  • Closed: 代表客户已经归还车辆并且已完成登记程序
JSON 来表示 CarRental 微服务的一种可选项是:
  1. {
  2. "Status":"Active",
  3. "RentalAgreementID":1234
  4. }
对于微服务 CarCheckin
  1. {
  2. "Status":"Closed",
  3. "RentalAgreementID":1234
  4. }
Status 字段可以从数据库中获取到,通过其 ID 加载租赁协议,但如果我们只想追踪其可用性,则直接在 JSON 消息中拥有会更简单且更高效。我们稍后会谈到这个。
一旦我们有了可发布者和他们发布的消息的格式,我们可以完成下图
CarAvailability 微服务将消费发送到“RentalAgreementSaved”主题的消息,因此如果 StatusClosed 则可用性增加1,如果 StatusActive 则将其减少1。
现在我们有一个能实现目标的工作系统,即计算汽车可用数。那我们可以扩展它来做其他有用的工作吗?我们真的可以应用 OCP 原则吗?

4.扩展系统

想象一下,我们希望在租赁流程结束后为客户生成发票。我们有一个 Invocing 微服务,我们让它订阅“RentalAgreementsSaved”主题,当 StatusClosed时,该服务可以从数据库获取租赁协议数据(租赁协议 ID 在消息中)以及租赁协议客户表的客户数据。有了所有的这些消息, Invoicing 微服务可以为客户开具发票。图片如下:
我们已经扩展了系统的功能,而没有修改它,只是利用了多个订阅者可以订阅相同的主题。所以,
OCP
原则可以在架构层面使用!

5.得墨忒耳定律

我们为自己的新能力兴奋,并希望添加新功能:向客户发送电子邮件,感谢他或她使用我们的服务。正如我们对 Invoicing 微服务做的那样,我们可以从数据库中获取租赁协议,然后从客户表连接的表中获取客户信息。但这不是很高效,我们的 CustomerThanking 服务与租赁协议无关。事实上,这并不遵守得墨忒耳定律,我们希望在我们的所有系统中都有良好的实践。
我们现在可以做的是修改“RentalAgreementsSaved”主题的消息内容,并添加一个“CustomerID”字段。JSON 如下:
  1. {
  2. "Status":"Closed",
  3. "RentalAgreementID":1234,
  4. "CustomerID":8965
  5. }
但是,等等,修改消息内容?我们现在正在打破 OCP原则!看来我们最终不得不放弃它。

6.有界上下文

好吧,我们仍然可以做一些事,领域驱动设计( DomainDrivenDesignDDD)将拯救我们。如果将域(domain)划分为有界上下文,则可以利用它。在我们正在研究的系统简化模型中,我们可以识别一下有界上下文:
  • 租赁协议
  • 客户
  • 车辆
  • 租赁代理:使用系统进行租赁协议的用户
  • 经纪人:通常,客户不直接租赁,而是通过经纪人租赁
所有的这些实体都在租赁协议中出现,但他们本身就是有界上下文。因此,在首次设计消息格式时,可以引入和我们正在执行的操作的主要有界上下文。在本例中,初始消息内容设计可以是:
  1. {
  2. "Status":"Closed",
  3. "RentalAgreementID":1234,
  4. "CustomerID":8965,
  5. "VehicleID":98263,
  6. "RentalAgent":24352,
  7. "Broker":6723
  8. }
有了这条消息,我们可以在不违反 OCP 原则的情况下实现 CustomerThanking 微服务,同时遵守得墨忒耳定律。更别说我们可以为以后出现的新业务构建基础。一些简单例子如:
  • 计算租赁代理手续费。
  • 经纪人的经济信息。
  • 任何与车辆维护有关的事情。
  • ……
用这种方式设计消息内容最重要的事是:我们打开了添加新功能的大门。这些功能是最初从未设计过的,且不会破坏已经存在的任何东西。

7.事件和消息数据

消息是如何组成的?消息中的必要数据是什么?
要回答这些问题,我们首先得知道我们正在处理的不同消息类型以及其目的。首先,消息代表了一个事件,是真实的,已经发生过的事情。当我们在主题中存储事件时,我们给该主题提供事件名。为了更好的理解事件,我推荐来自 Jonas Bonér 的这个演讲。
但是,事件的目的是什么?我知道事件的两种主要类型和它们的目的是:
  • 代表一个事实。
  • 构建数据流。
在我们描述的系统中使用的就是表示事实的事件。主要目的是传达已经发生的事,并提供对此事实有用的一些数据。我们只提供所需的信息,别的什么也不提供,良好的方式是提供与该事件相关的有界上下文的ID。
构建数据流的事件在大数据系统中使用,在这些系统中,系统拥有大量的信息遍历,你对其应用多次转换。在这种情况下,事件包含可以提供的尽可能多的信息,我们不希望系统在其他地方执行转换,因为这将产生额外的开销。

8.最小化消息信息

为什么最小化信息很重要?看一个例子: 想象一下,我们想要为系统添加一个新功能,一个 Recommendations微服务,将根据他或她的资料给客户发送可能报价的邮件。简单点,假设只需要客户的年龄来发起推荐。我们不想到数据库获取年,因为会产生额外开销,因此我们将其存储在消息中(暂时忘记 OCP原则,我们只是分析添加新数据到消息的影响)。
  1. {
  2. "Status":"Closed",
  3. "RentalAgreementID":5678,
  4. "CustomerID":8965,
  5. "VehicleID":98263,
  6. "RentalAgent":24352,
  7. "Broker":6723,
  8. "CustomerAge":27
  9. }
系统图现在如下:
我们因有一个良好的、解耦的系统而感动高兴。但是,它真的解耦嘛?想象一下,我们想要修改推荐算法,要考虑客户的驾照发布日期。很简单,只需要添加该字段到 JSON。但是,在这种情况下,我们的微服务没有真正解耦,所以每次需要一个字段时,我们必须修改订阅者和发布者!我们可能需要修改每个发布者和每个订阅者。我们的微服务紧密耦合,而且用了一个非常丑陋的方式,因为在改变系统前,我们都没有意识到这点。
我们可以认为,如果我们添加每个可能的字段到消息数据中,一切都将很美好,我们不需要修改发布者或订阅者。但系统会随着时间推移而发展,我们终将添加新字段到我们的模型,并需要修改每个微服务。因此,这个策略是不行的。
我们能做的最好的事情是在消息中提供足够的信息来完成我们在初始设计中考虑的用例,同时也使它可用于我们没有想到的新的微服务。一个好的起点是包含所涉及的主要有界上下文实体或与此事件通信相关的 ID。当然,这将打破得墨忒耳定律,新的微服务需要遍历多个实体,但这是我们需要做出的权衡。这就是软件架构的全部内容:如何做出良好的权衡以获得最佳的系统。遵循 OCP原则的能力是非常重要且有用的,但有时打破得墨忒耳定律是正当的选择。

9.总结

事件驱动系统为我们提供了一个很好的机会,可以在架构级应用开闭原则,利用我们已经完成的工作并以未知的方式扩展它。但是,我们需要仔细设计事件的内容,并意识到不好的设计会引入耦合的可能性。设计应该以系统的目的为指导,对一个目标的良好设计(例如,大数据系统中的数据流)对于另一个目标(反映事实的事件驱动系统)可能是一个糟糕的设计。领域驱动设计的有界上下文可以为我们提供一个消息内容设计的一些指导。架构是做出决策和权衡,最大化 OCP原则可能意味着最小化得墨忒耳定律,所以我们需要保存谨慎并找到平衡点。
原文链接:https://dzone.com/articles/the-open-closed-principle-at-an-architectural-leve
作者:David Llobregat
译者:Darren Luo


动手扫一扫关注,帮你不断突破技术壁垒
继续阅读
阅读原文