架构级开闭原则

Java基础

浏览数:522

2019-6-18

关注“锅外的大佬”微信公众号,每日推送国外技术好文

原文链接:https://dzone.com/articles/the-open-closed-principle-at-an-architectural-leve

作者:David Llobregat

译者:Darren Luo

1. 介绍

这是在架构级应用 SOLID 原则的系列文章的第一篇。如果你熟悉 OOP 中的类设计的 SOLID 原则,如果你想知道在设计系统架构的时候是否可以使用他们,我将尝试给你一些见解。

在类的级别,开闭原则(Open-Closed-Principle,OCP)表示一个类对扩展开放但对修改关闭,这意味着你应该能在不修改类的情况下扩展类的行为。这通常通过继承和组合扩展类来完成。

在架构级别,我们不尝试修改系统的一部分(最适合你架构的进程、守护程序、服务或微服务)的功能,而是利用你已经完成的工作添加新的部分。为了不修改现有部分,你的系统需要完全解耦。我将专注于服务通过消息队列进行通信的事件驱动系统。这可以是 ActiveMQ、RabbitMQ、ZeroMQ、Kafka 或任何其他服务,但我将使用 Kafka 的术语,例如主题(Topic)、发布者(publisher)和订阅者(subscriber),以及 Kafka 能让相同的主题有不同的订阅者。

2. 消息系统

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

3. 详细的例子

现在,让我们转到一个详细的示例,想象我们在汽车租赁共组工作,负责建立一个新的车辆可用性系统。这个租赁流程简化视图如下:

在租车过程中,签订租赁协议,客户取走汽车。汽车的可用性减少一。然后是客户使用车辆的时间(租赁时间),最后,我们必须考虑汽车的的归还和汽车的登记。发生这种情况时,汽车的可用性增加一。在者两种情况中,汽车租赁和汽车登记,我们保存一份租赁协议到数据库,因此我们可以定义一个事件,RentalAgreementSaved,这将在保存数据之后被触发。该事件将被存储在 RentalAgreementSaved 主题中。目前,我们有两个发布者发送消息到主题,微服务 CarRental 和 CarCheckin

现在,我们需要定义消息的内容。由于主题的目的是表达一个租赁协议已被保存,因此我们需要的最少量的信息是协议 ID。但该系统的主要目的是追踪车辆的可用性,因此最好有一个 Status 字段来帮助我们。该字段可以有两种可能的值:

  • Active。这代表车辆被另一个客户使用。
  • Closed。这代表客户已经归还车辆并且已完成登记程序。

用 JSON 来表示 CarRental 微服务的一种可选项是:

{
  "Status": "Active",
  "RentalAgreementID": 1234
}

对于微服务 CarCheckin

{
  "Status": "Closed",
  "RentalAgreementID": 1234
}

Status 字段可以从数据库中获取到,通过其 ID 加载租赁协议,但如果我们只想追踪其可用性,则直接在 JSON 消息中拥有会更简单且更高效。我们稍后会谈到这个。

一旦我们有了可发布者和他们发布的消息的格式,我们可以完成下图

CarAvailability 微服务将消费发送到“RentalAgreementSaved”主题的消息,因此如果 Status 是 Closed 则可用性增加一,如果 Status 为 Active 则将其减少一。

现在我们有一个能实现目标的工作系统,即计算汽车可用性。我们可以扩展它来做其他有用的工作吗?我们真的可以应用 OCP 原则吗?

4. 扩展系统

想象一下,我们希望在租赁流程结束后为客户生成发票。我们有一个 Invocing 微服务,我们让它订阅“RentalAgreementsSaved”主题,当 Status 为 Closed 时,该服务可以从数据库获取租赁协议数据(租赁协议 ID 在消息中)以及来自连接到租赁协议表的客户表的客户数据。有了所有的这些消息,Invoicing 微服务可以为客户开具发票。图片如下:

我们已经扩展了系统的功能,而没有修改它,只是利用了多个订阅者可以订阅相同的主题。所以,OCP 原则可以在架构层面使用!

5. 得墨忒耳定律

我们为自己的新能力自豪,并希望添加新功能,想客户发送电子邮件,感谢他或她使用我们的服务。正如我们对 Invoicing 微服务做的那样,我们可以从数据库中获取租赁协议,然后从客户表连接的表中获取客户信息。但是这不是很高效,我们的 CustomerThanking 服务与租赁协议无关。事实上,这并不遵守得墨忒耳定律,我们希望在我们的所有系统中都有良好的实践。

我们现在可以做的是修改“RentalAgreementsSaved”主题的消息内容,并添加一个“CustomerID”字段。JSON 如下:

{
  "Status": "Closed",
  "RentalAgreementID": 1234,
  "CustomerID": 8965
}

但是,等等,修改消息内容?我们现在正在打破 OCP 原则!看来我们最终不得不放弃。

6. 有界上下文

好吧,我们仍然可以做一些事,领域驱动设计(Domain Driven Design,DDD)将拯救我们。如果我们我们的域(domain)划分为有界上下文,我们可以利用它:在我们正在研究的系统的简化模型中,我们可以识别一下有界上下文:

  • 租赁协议。
  • 客户。
  • 车辆。
  • 租赁代理。使用系统进行租赁协议的用户。
  • 经纪人。通常,客户不直接租赁,而是通过经纪人租赁。

所有的这些实体都在租赁协议中出现,但他们本身就是有界上下文。因此,在首次设计消息格式时,我们可以引入和我们正在执行的操作的主要有界上下文。在本例中,初始消息内容设计可以是:

{
  "Status": "Closed",
  "RentalAgreementID": 1234,
  "CustomerID": 8965,
  "VehicleID": 98263,
  "RentalAgent": 24352,
  "Broker": 6723
}

有了这条消息,我们可以在不违反 OCP 原则的情况下实现 CustomerThanking 微服务,同时遵守得墨忒耳定律。更别说我们可以为以后出现的新业务构建基础。一些简单例子:

  • 计算租赁代理手续费。
  • 经纪人的经济信息。
  • 任何与车辆维护有关的事情。
  • ……

用这种方式设计消息内容最重要的事是我们打开了添加许多与此事件相关的新功能的大门,这些功能最初是从未设计过的,不会破坏已经存在的任何东西。

7. 事件和消息数据

消息是如何组成的?消息中的必要数据是什么?

要回答这些问题,我们首先得知道我们正在处理的不同消息类型以及其目的。首先,消息代表了一个事件,是真实的,已经发生过的一些事情。这是为什么过去我们命名我们的事件,他们都是已经发生的事情,我们无法改变。当我们在主题中存储我们的事件时,我们给该主题提供事件名。为了更好的理解事件,我推荐来自 Jonas Bonér 的这个演讲

但是,一个事件的目的是什么?我知道事件的两种主要类型和它们的目的是:

  • 代表一个事实。
  • 构建数据流。

在我们描述的系统中使用的就是表示事实的事件。主要目的是传达已经发生的事,并提供对此事实有用的一些数据。我们只提供所需的信息,别的什么也不提供,一个良好的搜寻方式是提供事件相关的有界上下文的 ID。

构建数据流的事件在大数据系统中使用,在这些系统中,系统拥有大量的信息遍历,你对其应用多次转换。在这种情况下,事件包含我们可以提供的尽可能多的信息,我们不希望我们的系统在其他地方执行转换,因为这将产生额外的开销。

8. 最小化消息信息

当我们想要描述事实是,为什么最小化信息很重要?让我们看一个例子。

想象一下,我们想要为系统添加一个新功能,一个 Recommendations 微服务,将根据他或她的资料给客户发送可能报价的邮件。让它简单一点,假设我们只需要客户的年龄来发起推荐。我们决定我们不想提供到数据库获取年龄的额外开销,因此我们将其存储在消息中(暂时忘记 OCP 原则,我们只是分析添加新数据到消息的影响)。

{
  "Status": "Closed",
  "RentalAgreementID": 5678,
  "CustomerID": 8965,
  "VehicleID": 98263,
  "RentalAgent": 24352,
  "Broker": 6723,
  "CustomerAge": 27
}

系统图现在如下:

我们很开心,因为我们有一个良好的、解耦的系统。但是,它真的解耦嘛?想象一下,我们希望修改我们的推荐算法,要考虑客户的驾照发布日期。很简单,我们只需要添加该字段进 JSON。但是,在这种情况下,我们的微服务没有真正解耦,所以每次我们需要一个字段时,我们必须修改订阅者和发布者!我们可能需要修改每个发布者和每个订阅者。我们的微服务紧密耦合,而且用了一个非常丑陋的方式,因为在我们需要改变系统前我们都意识不到这一点。

我们可以认为,如果我们添加每个可能的字段到消息数据中,一切都将良好,我们不需要修改发布者或订阅者。但系统会随着时间推移而发展,我们终将添加新字段到我们的模型,并需要修改每个微服务。因此,这个策略是不行的。

我们能做的最好的事情是在消息中提供足够的信息来完成我们在初始设计中考虑的用例,同时也使它可用于我们没有想到的新的微服务。一个好的起点是包含所涉及的主要有界上下文实体或我们与此事件通信相关的事实的 ID。当然,这将打破得墨忒耳定律,新的微服务需要遍历多个实体,但这是我们需要做出的权衡。这就是软件架构的全部内容,如何做出良好的权衡以获得最佳的系统。遵循 OCP 原则的能力是非常重要且有用的,有时打破得墨忒耳定律是正当的。

9. 总结

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

作者:锅外的大佬