第二十七章 服务:亦大和亦小

面向服务的“架构”和微服务“架构”最近变得非常流行。他们目前流行的原因包括以下几点:

  • 服务似乎彼此强烈分离。我们将会看到,这只是部分对的。
  • 服务似乎支持开发和部署的独立性。我们将再次看到,这只是部分对的。

服务架构?

首先,我们来考虑一下这样一个概念:使用服务的性质是一种架构。这显然是不真实的。系统的架构是由高层策略与低层细节分开的边界来定义的,并遵循依赖规则。简单地分离应用程序行为的服务仅仅是昂贵的函数调用,并不一定具有架构上的重要性。

这并不是说所有的服务都应该具有架构上的重要性。在跨流程和平台创建分离功能的服务方面通常有很大的好处,不管它们是否服从依赖规则。只是服务本身并没有定义一个架构。

一个有用的比喻是组织函数。单一或基于组件的系统的体系结构由跨越架构边界并遵循依赖规则的某些函数调用来定义。然而,这些系统中的许多其他功能只是将一种行为与另一种行为分开,而且在架构上并不重要。

所以它只是跟着服务。毕竟,服务只是跨进程和/或平台边界的函数调用。其中一些服务在架构上是重要的,有些则不是。我们的兴趣,在这一章,是前者。

服务好处?

上一个标题中的问号表示这一部分将挑战当前流行的服务架构的正统。让我们逐个分析“优点”。

解耦的问题

把系统分解成服务的一大好处就是服务彼此强烈地解耦。毕竟,每个服务都运行在不同的进程中,甚至不同的处理器上;因此这些服务不能访问彼此的变量。更重要的是,每个服务的接口都要有明确的定义。

这当然有一些事实——但不是很多。是的,服务在各个变量的层面上是解耦的。但是,它们仍然可以通过处理器或网络中的共享资源进行耦合。更重要的是,他们与他们分享的数据紧密相连。

例如,如果将新字段添加到在服务之间传递的数据记录,则每个在新字段上运行的服务都必须更改。这些服务还必须对该领域数据的解释保持一致。因此,这些服务与数据记录紧密耦合,因此间接相互耦合。

至于接口定义明确,这当然是正确的,但函数也是如此。服务接口不再是正式的,没有更严格的,没有比函数接口更好的定义。那么显然,这种好处是一种错觉而已。

独立开发和部署的问题

服务的另一个好处是他们可以由一个专门的团队把控和运营。该团队可以负责编写,维护和运行服务,作为开发运行策略(dev-ops strategy)的一部分。开发和部署的这种独立性被认为是可扩展的。相信可以从数十,数百甚至数千个可独立开发和部署的服务中创建大型企业系统。该系统的开发,维护和运行可以在相似数量的独立小组之间进行划分。

这个信念有一些说对了,但只有一些。首先,历史表明,大型企业系统可以从统一体和基于组件的系统以及基于服务的系统构建。因此,服务不是构建可扩展系统的唯一选择。

其次,解耦的问题意味着服务不能总是独立开发,部署和运行。只要数据或行为相互耦合,就必须协调开发,部署和运作。

猫咪问题

作为这两个问题的一个例子,让我们再看看我们的出租车聚合器系统。请记住,这个系统知道给定城市的许多出租车供应商,并允许客户下单乘坐。我们假设客户根据一些标准来选择出租车,比如接送时间,花费,豪华程度和乘坐体验。

我们希望我们的系统具有可扩展性,所以我们选择由很多微型服务来构建系统。我们将我们的开发人员细分为许多小团队,每个小团队负责开发,维护和运营相应的少量服务。

图27.1中的图表显示了我们的虚构架构师如何安排服务来实现这个应用程序。TaxiUI服务涉及使用移动设备订购出租车的顾客。TaxiFinder服务检查各种出租车供应商的库存,并确定哪些出租车可能是用户的候选人。它将这些数据存入附加给该用户的短期数据记录中。TaxiSelector服务根据用户的花费,时间,奢侈等标准,从候选人中选择合适的出租车。它把那辆出租车记录转到TaxiDispatcher服务处,该服务将之作为合适的出租车下单。

图27.1实现出租车聚合器系统的服务的组织

现在让我们假设这个系统已经运行了一年多了。我们的开发人员一直在愉快地开发新功能,同时维护和运行所有这些服务。

一个欢快的日子,营销部门与开发团队会面。在这次会议上,他们宣布他们计划向城市提供猫咪送货服务。用户可以下猫咪单将猫咪送到家中或商业场所。

该公司将在全市设立几个猫咪收集点。当有猫咪单时,将选择附近的出租车从这些收集点之一收集一只猫咪,然后将其送到适当的地址。

其中的一个出租车供应商已经同意参加这个项目。其他人可能会跟随。还有一些可能会观望。

当然,有些司机可能对猫过敏,所以这些司机不应该被选为这项服务的服务者。此外,一些顾客无疑也会有类似的过敏症,所以在过去3天内已经用来递送猫咪单的车辆不应该被选择服务有这种过敏症的顾客。

看看这个服务组织图。这些服务里有多少将不得不改变,以实现这个功能?全部都得变。显然,猫咪功能的开发和部署必须非常谨慎地协调。

换句话说,这些服务都是耦合的,不能独立开发,部署和维护。这是横切关注的问题。无论是否面向服务,每个软件系统都必须面对这个问题。图27.1中的服务组织图中描述的功能分解很容易受到贯穿所有这些功能行为的新功能的影响。

采用对象的修复方法

我们如何在基于组件的体系结构中解决这个问题?仔细考虑SOLID设计原则会促使我们创建一组可以通过多态扩展来处理新特性的类。

图27.2中的图表显示了该策略。这个图中的类大致对应于图27.1所示的服务。但是,请注意边界。还要注意依赖关系遵循依赖关系规则。

原始服务的大部分逻辑都保存在对象模型的基类中。然而,那些特定于乘坐单的逻辑部分已被提取到Rides组件中。猫咪的新功能已被放入Kittens组件。这两个组件使用模板方法或策略等模式覆盖原始组件中的抽象基类。

再次注意,这两个新组件Rides和Kittens遵循依赖规则。还要注意,实现这些功能的类是由UI控制下的工厂创建的。

显然,在这个方案中,当猫咪功能被实现时,TaxiUI必须改变。但是没有其他的东西需要改变。相反,新的jar文件或Gem或DLL被添加到系统中,并在运行时动态加载。

因此,猫咪的功能是解耦的,可独立开发和部署。

图27.2 使用面向对象的方法来处理交叉问题

基于组件的服务

显而易见的问题是:我们可以为服务做到吗?答案当然是:是的!服务不需要是很小的统一体。相反,服务可以使用SOLID原则进行设计,并给定一个组件结构,以便新组件可以添加到组件中,而无需更改服务中的现有组件。

将Java中的服务想象为一个或多个jar文件中的一组抽象类。将每个新功能或功能扩展都视为另一个包含扩展第一个jar文件中的抽象类的类的jar文件。然后,部署新功能不再是重新部署服务的问题,而是简单地将新的jar文件添加到这些服务的加载路径。换句话说,增加新功能符合开闭原则。

图27.3中的服务图显示了结构。这些服务仍然像以前一样存在,但每个服务都有自己的内部组件设计,允许将新功能添加为新的派生类。这些派生类作用在自己的组件中。

图27.3 每个服务都有自己的内部组件设计,使新功能可以作为新的派生类加入

交叉关切

我们所学到的是架构界限不限服务之间。相反,这些边界贯穿服务,将它们分解成组件。

为了处理所有重要系统所面临的交叉性问题,必须按照依赖规则设计内部组件架构,如图27.4所示。这些服务没有定义系统的架构边界;而是服务中的组件。

图27.4 服务必须设计为遵循依赖规则的内部组件架构

小结

尽管服务对于系统的可扩展性和开发能力来说是有用的,但它们本身不是架构上重要的元素。系统的体系结构由在该系统内绘制的边界以及跨越这些边界的依赖性来定义。该架构不是由元素通信和执行的物理机制所定义的。

服务可能是一个单一的组件,完全被架构边界所包围。或者,服务可能由几个按架构边界分隔的组件组成。在极少数情况下,客户和服务可能如此耦合,因此没有任何架构意义。

results matching ""

    No results matching ""