暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

谈谈架构层级的“开闭原则”

云时代架构 2019-05-16
364

点击上方"云时代架构", 右上角选择“设为星标”

精品技术文章准时送上!

来源:公众号EAWorld翻译发表

作者:David Llobregat  

译者:白小白 

原文:http://t.cn/E6FVsm6

原题:The Open-Closed Principle 

          at an Architectural Level

简介:

本文是关于架构层级SOLID原则的文章系列的第一篇。你可能熟悉如何在面向对象的层级遵循SOLID原则来进行类的设计,或者你也曾经疑惑这些原则是否适用于系统的架构设计,关于这一点,我将尝试给出一些我的见解。

在类的层级,开闭原则(the-Open-Closed-Principle,简称OCP原则)的含义是:一个类对扩展是“开”放的,而对变更是封“闭”的,意思是说,应该在不改变类的前提下扩展一个类的行为。而通常的方式是继承和多态。

在架构层级,我们并不会变更系统的一部分功能(可能是最适用于当前架构的进程,守护进程,服务,或者微服务),而是通过新增功能的方式来复用已完成的代码。为了不对现有的部分做出变更,系统需要做到完全的解耦。接下来的内容将聚焦于事件驱动系统,并以消息队列实现服务间通信。消息队列 可以是ActiveMQ, RabbitMQ, ZeroMQ, Kafka或者其他服务,我将以Kafka的话语体系来进行描述,如主题(Topic),发布者,订阅者,以及类似Kafka的多个订阅者共享相同主题的能力。

一、消息系统

上图是一个一般用例:发布者向主题发布消息(或者事件),多个订阅者可以从主题处获得该事件。箭头指示了通信的流向。假定发布者和订阅者都是微服务的话,双层的圆角矩形代表某一特定微服务的多个实例。在本例中的四个微服务:发布者,订阅者1,订阅者2,订阅者n,每个微服务都有多个实例。

二、具体示例

举一个具体的例子。假设我们在一家汽车租赁公司工作,并负责建立一个车辆的可用性系统。整个租赁流程的简化视图如下:

第1步,车辆租赁:包含租赁协议的签订和客户选车的过程。随即可用的车辆数减1。 

第2步,客户用车:客户在一定的时间范围内使用租赁的车辆。

第3步,车辆归还:车辆的归还和签到。随即可用的车辆数加1。

其中第1步和第3步都需要将租赁协议入库,因此我们可以设计一个事件,RentalAgreementSaved,在保存数据时触发。这一事件将被存储在RentalAgreementSaved主题中。因此到目前为止,共有两个发布者向主题发送消息,一个是CarRental微,另一个是CarCheckin微服务。

下面来定义消息的内容。鉴于本主题的意图是为了表征租赁协议的保存,因此所需的最小信息量即协议ID。但系统的使命是跟踪车辆的可用性,最好还是设置一个Status字段。这一字段可以有两个值:

激活状态。代表客户正在使用车辆。

关闭状态。代表客户已经归还了车辆并进行了签到。

CarRental微服务可以选用如下的JSON数据结构:

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

    CarCheckin微服务对应的数据结构是:

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

      Status字段本应从数据库读取,并通过协议ID加载租赁协议,但因为我们只关心车辆的可用性,直接在JSON消息中写入协议ID更简单,性能也更好。这一点在后文还会提到。

      有了消息发布者和消息的格式,我们就可以完成下面的图表:

      CarAvailability微服务会对发送给RentalAgreementSaved主题的消息进行消费:如果Status是“激活状态”,就减1,如果Status是“关闭状态”,就加1。

      现在已经有了能够完成既定目标的可用系统,可以计算车辆的可用性。这一系统是否可以扩展以适用其他有意义的工作?是否可以真的在此过程中应用开闭原则?

      三、系统扩展

      假定我们需要在租赁流程结束的时候,给客户开具发票。可以设计一个Invoicing微服务来订阅RentalAgreementsSaved主题消息(消息中附加了租赁协议的ID)。当Status是“关闭状态”时,发票微服务可以从数据库中读取租赁协议的数据,并从Customers表中读取用户数据(Customers表和RentalAgreements表是相关联的)。有了上述信息,Invoicing微服务将可以向用户提供发票。过程如下图所示:

      我们扩展了系统的功能,但并没有变更系统的代码。只是利用了多个订阅者可以订阅同一个消息主题的机制。因此是的,OCP原则可以在架构层级得以应用。

      迪米特法则

      迪米特法则(Law of Demeter)又叫作最少知识原则(Least Knowledge Principle 简称LKP),就是说一个对象应当对其他对象有尽可能少的了解。

      假设,我们对现有的功能很满意,并准备添加一个新的功能:向用户发出感谢信来感谢他或她使用了我们的服务。参考发票微服务的例子,我们可以同样从数据库中获得租赁协议和用户数据。但这样的设计效率不高,因为CustomerThanking服务根本不需要用到租赁协议的内容。事实上,这也违反了“迪米特法则”,而我们希望所有的系统都是符合良好的架构实现的。

      也许我们可以这样,变更RentalAgreementsSaved主题的消息内容,添加一个“CustomerID”字段,JSON格式数据如下:

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

        呃,等等,不是说好了要应用OCP原则的么,怎么能变更消息内容呢。看来还真是这样,好吧,我们得想想别的办法。

        有界上下文

        还好,确实有其他方案可供选择。我们可以让领域驱动设计(DDD)来帮忙。只要将领域拆分成有界上下文,就可以利用其优势来完成工作。在一个超简化的系统模型中,我们可以定义如下的有界上下文:

        租赁协议 客户  车辆。

        租赁专员:使用系统来办理租赁协议 租赁中介:通常情况下客户并不直接租车,而是通过代理人来租车

        所有这些实体都出现在租赁协议中,但又自成有界上下文。因此,在最初设定消息格式的时候,我们可以根据正在进行的操作来引入主要的有界上下文。在本例中,初始的消息内容设计应该是下面的样子:

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

          有了这样的消息结构,我们总算可以在不打破OCP原则和“迪米特法则”的情况下实现CustomerThanking微服务了。更不用说,我们还可以应用这样的消息结构来应付将来新的业务需要。比如:

          计算租赁专员佣金。  提供租赁代理的经济信息。  车辆维护事宜。  诸如此类。

          最重要的是,通过这样设计的消息内容,我们打开了一扇门,可以添加大量与这个事件相关的新功能(这些功能我们不可能在一开始就想清楚),而不需要改变现存的代码。

          四、事件和消息数据

          消息是如何构成的?消息的必要内容是什么?

          为了回答这些问题,我们首先需要了解消息类型和目标的差异。首先,消息表征了事件,而事件即事实,代表已经发生的事情。我们在既往的时间定义了事件,这些事件代表已经发生过的事情,我们无法改变既成事实。我们在Topic中存储事件的时候,以事件为Topic命名。为了更好的理解事件,我推荐Jonas Bonér的演讲。

          (链接:http://t.cn/EXlZNA5,英文)

          事件的目标又是什么呢?我所知道的两种类型事件的目标是这样的:

          表征事实。
          建立数据流。

          用来表征事实的事件,用于如前文所述的系统之中。其主要的目标就是传达某事已经发生的消息,并提供与这一事情相关的有用的数据。我们尝试不多不少地仅提供所需的信息,比如仅提供与事件相关的有界上下文实体的ID。

          用于为系统构建数据流的事件,一般是应用于大数据系统中。在大数据系统中充斥着横跨系统范围内大量的信息,信息在传递过程中会经过多次转换。可以让事件附加我们所能提供的尽量多的信息,以便减少在信息转换所带来的额外的成本。

          最小化消息信息

          为什么在表征事实的时候,最小化信息量是如此的重要?我们来看一个例子。

          假设我们需要为系统添加一个新的功能:一个Recommendations的微服务,来向客户发送一封邮件,并根据客户的个人资料推荐一些优惠信息。简化起见,假定我们只需要客户的年龄信息来进行推荐。为了不产生额外的开销,我们可以选择不读数据库,而是把年龄数据存储在消息中(此处先不考虑OCP原则,我们只是在分析在消息中添加新数据产生的影响)。

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

            系统示意图如下:

            看起来我们有了一个良好解耦的系统,这很好。但是,事实真的是这样么?假定我们需要修改推荐算法,把用户驾照的发证信息纳入考虑。很简单,只需要把这个字段加到JSON里即可。但在本例中,我们的微服务并没有实际解耦,因此,每次我们需要一个新的字段的时候,就需要同时修改订阅者和发布者。我们的微服务是紧耦合的,并且是以一种很丑陋的方式,直到我们需要对系统做出变更的时候,我们才会意识到这一点。

            我们可能会这样想,是不是可以把每个可能的字段都加到消息数据中,这样问题就解决了,我们再也不需要修改发布者和订阅者了。但系统是随时间进化的,总会有一个时刻,需要在模型中添加新的字段,同时需要修改每一个微服务。因此这条路也行不通。

            我们所能采取的最好的方案是,在消息中提供足够的信息,既满足我们最初的设计中所考虑到的用例场景的需要,同时也要让这些信息对我们可能尚未考虑到的新的服务是可用的。比如把主要的有界上下文的实体的ID引入 进来,这些实体要么是我们与事件通信的事实的一个组成部分,要么与之相关。新的微服务需要涵盖很多的实体,当然,这会打破“迪米特法则”,但这是必要的妥协。软件架构的要义即在于此:做出恰当的权衡以达成尽量好的系统。遵循开闭原则的能力是如此的重要而有意义,从某种程度上讲,我们可以为此不惜违背“迪米特法则”。

            五、总结

            1、事件驱动系统给了我们很好的机会来在架构层级应用开闭原则。我们可以重用已有的代码,并且在未知的方向上实现功能的扩展。

            2、然而,需要谨慎的设计事件的内容,同时警惕糟糕的设计可能引入的耦合的可能性。

            3、要根据系统的目标来指导架构设计,为某一目标设计某种适用的架构(如,为大数据系统设计的流式数据)很可能对于另一目标来说是糟糕的设计(不适用于表征事实发生的事件驱动系统)。

            4、领域驱动设计的有界上下文可以为事件内容的设计提供一些指导。

            5、架构是关于决策和权衡,最大化应用开闭原则很可能意味着对“迪米特法则”的最小化遵循,必须谨小慎微地寻找一个平衡点。

            微服务配置中心全面对比,哪个更牛逼!?
            最全的微服务知识科普(文末送书)
            如何保障消息中间件100%消息投递成功?如何保证消息幂等性?
            一篇文章读懂HTTPS及其背后的加密原理
            互联网架构:屡试不爽的架构三马车
            支付系统订单模型该如何设计?
            第三方支付账务系统论述
            面试官:给我说说你使用Mybatis的过程中遇到过哪些坑?

            【福利时间】

            1. 为感谢云时代架构粉丝的厚爱,连续十期赠送云时代架构系列图书任意两本

            2. 参与方式:扫描抽奖小程序二维码,即可参与抽奖活动!

            3. 第五期活动时间:即日起至5月19日21:00止。中奖者请及时加微信号cynthia_ld,超过48小时未联系的视为弃权;

            快扫描上方二维码参与活动吧!

            做互联网时代适合的架构:开放、分享、协作

            长按二维码即可关注我们

            在看|求转发

            文章转载自云时代架构,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

            评论