第十八章 边界剖析

系统的体系结构由一组软件组件和将它们分开的边界来定义。这些边界有许多不同的形式。在本章中,我们将看一些最常见的。

边界跨越

在运行时,边界跨越无非就是在边界的一侧的函数调用另一侧的函数并传递一些数据。创建适当的边界跨越的技巧是管理源代码依赖关系。

为什么是源代码?因为当一个源代码模块发生变化时,其他源代码模块可能需要更改或重新编译,然后重新部署。管理和构建防火墙来应对这种变化是就是边界的意义所在。

忧虑的统一体(MONOLITH)

最简单和最常见的架构边界没有严格的物理呈现的。它只是在一个处理器和一个地址空间内按规定进行的功能和数据分离。在前面的章节中,我将其称为源代码级解耦模式。

从部署的角度来看,这只不过是一个单一的可执行文件,即所谓的统一体。该文件可能是一个静态链接的C或C++项目,一组绑定在一个可执行的jar文件中的Java类文件,一组绑定到一个.EXE文件的.NET二进制文件等等。

事实上,在一个统一体的部署过程中,这些边界是不可见的,这并不意味着它们不是现在的和有意义的。即使静态链接到单个可执行文件中,独立开发和编组各种组件的能力也是非常有价值的。

这样的体系结构几乎总是依赖某种动态多态来管理它们的内部依赖关系。这是近几十年来面向对象开发已成为如此重要的一个范例的原因之一。如果没有面向对象或同等形式的多态性,架构师必须回避使用函数指针来实现适当解耦的危险做法。大多数架构师发现使用指针函数的风险太大,所以他们被迫放弃任何类型的组件分区。

最简单的边界跨越是从低层的客户端到更高层服务端的函数调用。运行时依赖关系和编译时间依赖关系指向相同的方向,朝向更高级别的组件。

在图18.1中,控制流程从左到右跨越边界。Client在Service上调用函数f()。它传递一个Data实例。<DS>标记只是表示一个数据结构。Data可以作为函数参数或其他一些更精细的方法传递。请注意,数据的定义位于边界的被调用方。

图18.1 控制流程从较低层跨越边界到较高层

当一个高层的客户端需要调用一个低层的服务时,动态多态是用来反转对控制流程的依赖。运行时依赖反对编译时间依赖。

在图18.2中,控制流程像以前一样从左向右跨越边界。高层Client通过Service接口调用低级ServiceImpl的f()函数。但是请注意,所有的依赖关系都是从右到左越过边界到更高层的组件。还要注意,数据结构的定义在边界的调用方。

图18.2 边界跨越与控制流

即使在一个统一体的,静态链接的可执行文件中,这种有规则的分区也可以极大地帮助开发,测试和部署项目。团队可以在彼此独立的组件上彼此独立工作,而不会相互不小心地影响。高层组件保持独立于较低层的细节。

统一体之间的通信非常快速而且低成本。它们通常只是函数调用。因此,跨越源代码级别的解耦边界的通信可能非常好相处(chatty)。

由于统一体的部署通常需要编译和静态链接,因此这些系统中的组件通常作为源代码提供。

部署组件

架构边界最简单的物理表示是动态链接库,如.Net DLL,Java jar文件,Ruby Gem或UNIX共享库。部署不涉及编译。相反,组件是以二进制形式交付的,或者是一些等同的可部署形式。这是部署级别的解耦模式。部署的行为只是将这些可部署单元以一些便利的形式(例如WAR文件,甚至仅仅是一个目录)聚集在一起。

有一个例外,部署级别的组件与统一体相同。这些函数通常都存在于相同的处理器和地址空间中。分离组件和管理依赖的策略是一样的。

与统一体相同,跨部署组件边界的通信只是函数调用,因此非常低成本。对于动态链接或运行时加载可能存在仅一次性命中1,但跨越这些边界的通信仍然非常好相处。

线程

统一体和部署组件都可以使用线程。线程不是架构边界或部署单元,而是一种组织计划和执行顺序的方法。它们可能完全包含在一个组件中,或者遍布在许多组件中。

本地进程

一个更强大的物理架构边界是本地进程。通常通过命令行或等效的系统调用来创建本地进程。本地进程在同一个处理器中运行,或者在多核内的同一组处理器中运行,但是运行在不同的地址空间中。内存保护通常会阻止这些进程共享内存,尽管可以使用共享内存分区。

大多数情况下,本地进程使用套接字或其他类型的操作系统通信工具(如邮箱或消息队列)相互通信。

每个本地进程可以是一个静态链接的整体,也可以是动态链接的部署组件。在前一种情况下,几个统一体进程可能有相同组件被编译并链接到其中。在后者中,他们可能共享相同的动态链接部署组件。

将本地进程看作是一个超级组件:该进程由较低级别的组件组成,通过动态多态来管理它们的依赖关系。

本地进程之间的隔离策略与统一体和二进制组件相同。源代码依赖关系指向跨越边界的相同方向,并始终指向更高级别的组件。

对于本地进程,这意味着更高层进程的源代码不能包含更低层进程的名称,物理地址或注册表查找键。请记住,架构目标是让较低层流程成为更高层流程的插件。

跨本地进程边界的通信涉及操作系统调用,数据封送和解码以及处理器上下文切换,这些切换非常高成本。处理时应该小心地限制。

服务

最强的边界是服务。服务是一个过程,通常从命令行或通过等效的系统调用开始。服务不取决于他们的物理位置。两种通信服务可能或不可能在同一物理处理器或多核中运行。这些服务假定所有的通信都通过网络进行。

与函数调用相比,跨服务边界的通信非常缓慢。周转时间可以从几十毫秒到几秒。必须小心避免在可能的情况下通信。这个级别的通信必须处理高度的延迟。

否则,相同的规则适用于服务适用于本地进程。较低层的服务应该“插入”更高层的服务。高层服务的源代码不得包含任何低层服务的具体物理信息(例如URI)。

小结

除了统一体之外,大多数系统都使用多个边界策略。使用服务边界的系统也可能具有一些本地进程边界。事实上,服务往往只是一组相互作用的本地流程的立面。服务或本地进程几乎可以肯定是由源代码组件组成的整体或者一组动态链接的部署组件。

这意味着系统中的边界往往是混杂的本地通信的边界和更在乎延迟的边界。

1. note: 可能存在第二次访问时缓存已被清理,需要再次加载的情况,也就是用时再加载。这种情况使得曾经缓存过的变得毫无意义。

results matching ""

    No results matching ""