在某些时候,工程师必须绘制一些方框和箭头来描述软件系统的顶层设计。但是,这些方框和箭头叫什么?我们经常使用诸如微服务,实体,REST 或事件驱动之类的术语,这些又是什么?
作为研究论文的一部分,我一直在阅读有关软件体系结构概念和定义的知识,并且在整篇文章中,我将解释其中的一些概念,这些概念适用于我在研究生期间也一直在从事的项目:JSON- RPC Playground 控制台。
软件架构是什么?
我将使用 Roy Fielding(HTTP 规范的主要作者之一和 REST 风格的创建者)在其博士学位论文中给出的定义(如果您对 Software Architecture 感兴趣,我推荐您看看这篇论文)。
软件体系结构是软件系统在其操作的某个阶段的运行时元素的抽象。一个系统可能由许多抽象级别和许多操作组成,每个阶段都有自己的软件体系结构。
架构是抽象的
在描述体系结构时,可以对实现细节进行抽象,以简化设计。如用 Elixir 或 Java 编写身份验证服务有关系吗,显然这不是在架构层面要考虑的事情?还是它在系统中扮演的角色-验证用户-我们应该关注什么?
架构与运行时有关
源代码结构不是系统的体系结构。当系统处于活动状态时,不同的应用程序可以共享公共库或模块,但彼此之间完全断开连接,这个是低耦合的设计。我们专注于处理数据以及如何移动数据。软件架构一般用高内聚、低耦合方式构建。
架构专注于特定的阶段
并非每个组件都会始终发挥作用或成为各个流程的一部分。系统关闭时涉及的组件及其配置可能与系统正常操作模式中涉及的组件及其配置完全不同。
架构是嵌套的
在抽象元素实现时,我们将忽略与当前架构无关的细节。如果我们将重点转移到更深层次,则将出现具有其自身的元素和配置集的新体系结构。我们将找到许多嵌套的体系结构,直到元素足够简单以至于无法分解为止,实现原子性构建。
一系列决策的结果是创建了一个软件体系结构,每个决策都带来了一组属性和约束。无论它们在图表中是明确指出的,还是仅存在于架构师和开发人员的思想中,对于这些决定和约束应该都有一种理解或意图。随着系统的发展,这些最初的决定很可能与架构的实际情况不符。如果这些差异是不希望的或偶然的,那么我们说该体系结构已被侵蚀,造成了体系结构的“技术负债”。
案例研究:JSON-RPC 控制台
我们的客户平台之一是由数十个 JVM 微服务组成,它们使用 JSON-RPC 2.0 协议相互通信。每个服务使用一组 Java 接口声明其 RPC API,这些 Java 接口作为“ Service-API”库(JAR)发布在公共存储库中。想要与服务进行交互的客户端只需将其 API JAR 声明为项目依赖项即可。平台库将生成实现此类接口的对象,并通过依赖注入提供实现代码类。从代码角度来看,您只是在调用常规方法,但是在后台,平台库正在执行 RPC 调用并为您处理所有涉及的管道。这在编写代码时极大地提高了工作效率!
但是,要手动测试所有这些 RPC 方法(例如,使用诸如 Postman 或 curl 的工具),就必须找到正确的代码库,手动检查服务接口,其方法和参数(可能具有许多嵌套对象级别) ),然后手动构建所需的 JSON payout 以执行 API 调用。一般而言,API 文档有帮助,但是很难保持最新,这是一个问题。
这里,我创建一个 GUI 应用程序,该应用程序会自动生成可轻松填充的表单,以调用服务公开的任何 RPC 方法。这些表单是通过与 JSON-RPC 2.0 兼容的服务描述文件生成的,该文件是通过分析 Service-API JAR 库创建的。通过使用与生产中运行的实际代码相同的源,可以确保它们保持最新。
架构元素
构架系统意味着要做出一系列决定,这些决定可以塑造构成系统的不同元素(组件,连接器和数据)的配置。
组件(Components)
组件是软件指令和内部状态的抽象单元,它通过接口提供转换或执行数据计算。组件是由它们向其他组件提供的服务定义的,而不是由它们的界面后面的实现定义的。如果其他组件无法识别某些行为,则该行为不是体系结构的一部分。
示例
RPC 控制台:将服务描述转换为一组表单,捕获用户输入,执行 RPC 调用并显示其结果。
RPC 服务器:接收 RPC 请求,对其进行计算,然后返回结果。
分析器:将 Service-API JAR 转换为服务描述。
JAR 存储库:存储并提供 Service-API JAR。
服务说明存储库:存储并提供服务说明。
请注意,就此体系结构的观点而言,在定义 RPC Server 组件时,我们对 RPC Server 提供的特定功能不感兴趣,因为它与其余组件无关。我们甚至将这个组件的许多不同实例均等地分组,即使实际上它们在功能上会有很大不同。如,一个可能是 Users 服务,而另一个可能是 Books 服务。
连接器(Connectors)
连接器可实现不同组件之间的通信和数据传输。他们不转换数据,而是通过界面在不同组件之间对数据进行移动。但是在内部,当查看一个特定连接器的体系结构时,我们可能会发现它实际上是由一个子系统组成的,这些子系统接收数据,将其转换为更好的格式以进行传输,将其发送到另一端,然后反转转换,然后再传递给系统的其余部分。由于这些转换对系统的其余部分不可见,因此我们可以将它们抽象化为更高的层次。
在示例中:
RPC 客户端:开始 RPC 调用。
RPC 服务器:接收 RPC 请求并返回 RPC 响应。
HTTP 客户端:启动 HTTP 连接以获取服务描述。
AWS 库:将服务描述从分析器传输到服务描述存储库。
Gradle 库:将 Service-API JAR 依赖项从 JAR 存储库传输到分析器。
对于 AWS Library 和 Gradle Library 而言,我们不直接负责这些数据传输的方式。然后,我们可以使用连接器的视图,而忽略其实现的细节。
数据 (data)
许多软件体系结构定义没有将数据作为核心概念提及,我认为这并不完整。数据是系统存在的原因,有时甚至是驱动系统配置的主要因素。数据定义为通过连接器从一个组件传输到另一组件的信息。
在示例中:
服务描述:以 JSON-RPC 2.0 兼容结构描述服务公开的可用 RPC 方法。它包括服务器 URL,方法名称,参数和类型之类的信息。
RPC 请求:包括 RPC 方法名称及其参数。
RPC 结果:RPC 调用执行的结果。
Service-API JAR:包含 RPC 服务的 Java 接口的 JAR 文件。
架构风格
架构风格是架构设计决策的命名集合,当在特定上下文中应用时对应不同的系统元素,它们的配置以及它们之间的关联方式施加约束,进而生成具有众所周知架构解决方案。
样式是一种用于对体系结构进行分类并定义其共同特征的机制。每种样式都为组件的交互提供了抽象,通过忽略架构其余部分的细节来捕获交互模式的本质。样式可以仅关注体系结构的某些方面,甚至可以将它们组合以生成更复杂的样式或混合样式。
客户端-服务器,微服务,Monolithic 甚至是 REST 都是不同的体系结构样式,您很可能已将其应用于数十种异构系统。
创造自己的风格
如果您熟悉诸如Swagger的 REST API 之类的工具,您可能会注意到我的 JSON-RPC 项目与之相似。虽然我的控制台使用了针对基于 JSON-RPC 的服务量身定制的服务描述作为输入,但是 REST API 具有OpenAPI标准。从服务的源代码生成规范格式是一种强大的模式,可用于创建许多不同的使用者工具:文档导航器,客户端代码生成器,模拟服务器等。
让我们尝试为该工具系列定义通用的体系结构样式,该样式可以应用于任何其他协议以获得相同的好处:我将其称为“服务描述”样式。
服务风格描述
让我们开始定义架构的不同元素
数据元素:
目标源代码:目标服务接口的源代码。
服务描述:特定于协议的格式,遵循协议标准,可以描述任何目标服务的接口。
组件:
生成器:自动从目标源代码创建服务描述,并将其发布到提供者。
存储库:存储并提供服务说明。
客户端:使用存储库中的服务描述,并将其用作提供针对目标服务动态定制功能的唯一来源。
连接器(connectors):
生成器->存储库:将服务描述从生成器传输到存储库。
存储库->客户端:将服务描述从存储库传输到客户端。
必须从源代码创建服务描述。客户端需要始终保持最新的服务说明才能正常运行,因为除非服务说明中包含客户信息,否则他们对目标服务的具体情况一无所知。主要来源是代码,如果流程不是自动化的,则很可能会出现服务描述过时且客户端损坏的风险。这并不意味着不能手动构建服务描述。这样做有很多有效的用例,例如,如果您想在实际实现之前拥有一个模拟服务器。但是,依赖于手动任务的系统将不被视为该体系结构样式的实现。
请注意,我们对生成器如何使用源代码没有任何限制。实际上,生成器甚至可以作为目标构建过程中的一个步骤来实现(例如,使用 Maven 插件)。服务描述应遵循协议标准。该体系结构的主要优点之一是客户端可针对使用同一协议的许多不同目标服务进行重用,因此,服务描述无法了解仅适用于一项特定服务的特定实现细节。客户端提供的功能不属于体系结构的一部分:客户端可以与目标服务(例如,用于 Playground 控制台)进行交互,或者根本不进行交互(对于静态文档而言)。客户背后的主要限制是,除了服务描述所包含的信息外,他们还应该对目标服务的实施细节一无所知。连接器的定义非常宽松,因为我们对信息的传输方式没有任何限制。
评论 1 条评论