区分 REST 架构风格和其他基于网络风格的核心特征是它强调组件之间的统一接口。通过将软件工程的通用性原则应用于组件接口,简化了整体系统架构,提高了交互可见性。实现与它们提供的服务解耦,从而鼓励独立演化。
-Roy Fielding, https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5
在本篇文章中,我们将探讨在 Web 应用中两种不同的解耦类型。
我们将看到,在应用程序级别,超媒体 API 将你的前端和后端紧密耦合。尽管如此,令人惊讶的是,超媒体 API 在面对变化时实际上更具弹性。
耦合 是一个软件系统属性,其中系统的两个模块或方面具有高度的相互依赖性。解耦 软件是指减少不相关模块之间的这种相互依赖性,以便它们能够彼此独立地演化。
耦合和解耦的概念紧密相关(并且呈反比)内聚。高度内聚的软件在模块或概念边界内具有相关的逻辑,而不是分散在整个代码库中。(一个相关概念是我们自己对行为局部性的理解)
总体而言,经验丰富的开发人员努力构建解耦且内聚的系统。
如今,构建 Web 应用的一种常见方法是创建一个 JSON 数据 API,然后使用 JavaScript 框架(例如 React)来使用该 JSON API。这种应用程序级架构决策将前端代码与后端代码解耦,并允许在其他环境中重复使用 JSON API,例如移动应用、第三方客户端集成等。
这是一个应用程序级解耦,因为解耦的决策和实现是由应用程序开发人员自己完成的。JSON API 在两段软件之间提供了一个“硬”接口。
以我喜欢的例子为例,考虑一个简单的银行 JSON,它在https://example.com/account/12345
处有一个GET
端点。此 API 可能会返回以下内容
HTTP/1.1 200 OK
{
"account": {
"account_number": 12345,
"balance": {
"currency": "usd",
"value": -50.00
},
"status": "overdrawn"
}
}
此数据 API 可以被任何客户端使用:Web 应用、移动客户端、第三方等。它与任何特定客户端解耦。
到目前为止,一切都很顺利。但是这种解耦在实践中如何运作呢?
在我们之前的文章拆分你的数据和应用程序 API:更进一步中,你会发现以下引述
我这些天工作中最糟糕的部分是为前端开发者设计 API。对话总是不可避免地变成
开发人员 - 因此,这个屏幕有数据元素 x、y、z……你能创建一个具有响应格式 {x: , y:, z: } 的 API 吗?
我 - 好的
Jean-Jacques Dubray - https://www.infoq.com/articles/no-more-mvc-frameworks
这段话表明,虽然我们已经用干草叉(或者在我们的案例中,用 JSON API)将耦合驱逐出去,但它又通过对特定于 Web 应用的 JSON API 端点的请求回来了。这类请求最终会导致前端和后端代码重新耦合:JSON API 不再提供通用 JSON 数据 API,而是提供一个针对前端需求的特定 API。
更糟糕的是,这些前端需求会随着应用程序的不断发展而频繁变化,因此需要修改你的 JSON API。如果其他非 Web 应用客户端已经依赖于原始 API 会怎样?
这个问题会导致许多 JSON 数据 API 开发人员在支持 Web 应用以及其他非 Web 应用客户端时面临的“版本地狱”。
解决这个问题的一个潜在方案是引入GraphQL,它允许你拥有一个更具表现力的 JSON API。这意味着当你的 API 客户端的需求发生变化时,你不需要经常更改它。
这是一种解决上述问题的合理方法,但它存在一些问题。我们看到最严重的问题是安全性,就像我们在API churn/安全权衡文章中所述。
显然,facebook 使用了一个白名单来解决 GraphQL 引入的安全问题,但许多使用 GraphQL 的开发者似乎并不了解与之相关的安全威胁。
Max Chernyak 在他的文章不要构建一个通用目的 API 来为你的前端提供动力中推荐的另一种方法是构建两个 JSON API
这是一个务实的解决方案,可以解决你 Web 应用的前端与其支持的后端代码之间的固有耦合问题,并且它不涉及通用 GraphQL API 中的安全权衡。
现在让我们考虑一下超媒体 API如何解耦软件。
考虑对我们上面看到的相同GET
(https://example.com/account/12345
)的潜在响应
HTTP/1.1 200 OK
<html>
<body>
<div>Account number: 12345</div>
<div>Balance: $100.00 USD</div>
<div>Links:
<a href="/accounts/12345/deposits">deposits</a>
<a href="/accounts/12345/withdrawals">withdrawals</a>
<a href="/accounts/12345/transfers">transfers</a>
<a href="/accounts/12345/close-requests">close-requests</a>
</div>
<body>
</html>
(是的,这是一个 API 响应。它碰巧是一个超媒体格式的响应,在本例中是 HTML。)
在这里我们看到,在应用程序级别,这种响应与“前端”的耦合程度不可能更高。实际上,它就是前端,因为 API 响应不仅指定了资源的数据,还提供了关于如何准确地将此数据显示给用户的布局信息。
响应还包含超媒体控制,在本例中是链接,最终用户可以从中选择以继续浏览此超媒体驱动的应用程序提供的超媒体 API。
那么,在这种情况下,解耦在哪里呢?
在这种情况下,解耦发生在更低级别。它发生在网络架构级别,也就是说,在系统级别。超媒体系统旨在将超媒体客户端(在 Web 的情况下,是浏览器)与超媒体服务器解耦。
这主要通过 REST 的统一接口约束来实现,特别是通过使用超媒体作为应用程序状态的引擎(HATOEAS)。
这种解耦风格允许在更高的应用程序级别进行更紧密的耦合(我们已经看到这可能是固有的耦合),同时仍然保留对整个系统的解耦益处。
这种解耦在实践中如何运作?好吧,假设我们希望移除从我们的银行转账到其他银行以及关闭账户的功能。
现在,我们对这个GET
请求的超媒体响应是什么样的?
HTTP/1.1 200 OK
<html>
<body>
<div>Account number: 12345</div>
<div>Balance: $100.00 USD</div>
<div>Links:
<a href="/accounts/12345/deposits">deposits</a>
<a href="/accounts/12345/withdrawals">withdrawals</a>
</div>
<body>
</html>
你可以看到,在这个响应中,这两个操作的链接已经从 HTML 中移除。浏览器只是将新的 HTML 呈现给用户。在舍入误差范围内,没有客户端在使用旧的 API。API 编码在超媒体中,并通过超媒体发现。
这意味着我们可以大幅度更改 API,而不会破坏我们的客户端。
这种灵活性是 REST 式网络架构的核心,特别是HATEOAS的核心。
正如你所见,尽管我们自己的前端和后端之间存在着更紧密的应用程序级耦合,但由于 REST 式超媒体系统的统一接口方面提供的网络架构解耦,我们实际上拥有了更大的灵活性。
许多人会反对,当然,这个超媒体 API 可能对我们的 Web 应用来说很灵活,但它对于通用目的 API 来说太糟糕了。
这的确是事实。这个超媒体 API 是针对特定 Web 应用调整的。尝试下载这个 HTML、解析它并尝试从中提取信息将非常麻烦且容易出错。这个超媒体 API 只有作为更大超媒体系统的一部分才有意义,并且由合适的超媒体客户端使用。
这正是我们建议在拆分你的数据和应用程序 API:更进一步中创建通用目的的 JSON API 以及你的超媒体 API 的原因。你可以利用超媒体对自己的 Web 应用的灵活性,同时为移动应用、第三方应用等提供通用目的的 JSON API。
(虽然,我们应该提一下,一个基于超媒体的移动应用也可能是一个不错的选择!)
在这篇文章中,我们探讨了两种不同的解耦类型
并且我们看到,尽管基于超媒体的应用程序中存在更紧密的应用程序级耦合,但正是超媒体系统能够更优雅地处理变化。