两种解耦方法

Carson Gross

REST 架构风格与其他基于网络的风格的主要区别在于它强调组件之间的一致接口。通过将软件工程的一般性原则应用于组件接口,整个系统架构得到简化,交互的可见性得到改善。实现与它们提供的服务解耦,这鼓励了独立的演化能力。

-Roy Fielding, https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5

在本文中,我们将考察 Web 应用程序上下文中两种不同类型的解耦:

我们将看到,在应用程序级别,超媒体 API 会紧密耦合你的前端和后端。尽管如此,令人惊讶的是,超媒体 API 在面对变化时实际上更具弹性。

耦合

耦合 是软件系统的一个属性,其中系统的两个模块或方面具有高度的相互依赖性。解耦 软件是指减少不相关模块之间的这种相互依赖性,从而使它们能够独立演化。

耦合和解耦的概念与 内聚性 密切相关(并且是反向的)。高度内聚的软件在模块或概念边界内包含相关逻辑,而不是散布在整个代码库中。(一个相关概念是我们自己的 行为局部性 想法)

总的来说,经验丰富的开发者努力实现解耦和内聚的系统。

JSON 数据 API - 应用程序级别解耦

当今构建 Web 应用程序的一种常见方法是创建一个 JSON 数据 API,然后使用 React 等 JavaScript 框架消费该 JSON API。这种应用程序级别的架构决策将前端代码与后端代码解耦,并允许在其他上下文中重用 JSON API,例如移动应用程序、第三方客户端集成等。

这是一个 应用程序级别 的解耦,因为解耦的决策和实现是由应用程序开发者自己完成的。JSON API 在两部分软件之间提供了一个“硬”接口。

使用我最喜欢的例子,考虑一个银行的简单 JSON,其 GET 端点位于 https://example.com/account/12345。此 API 可能返回以下内容:

HTTP/1.1 200 OK

{
    "account": {
        "account_number": 12345,
        "balance": {
            "currency": "usd",
            "value": -50.00
        },
        "status": "overdrawn"
    }
}

此数据 API 可以被任何客户端消费:Web 应用程序、移动客户端、第三方等。因此,它与任何特定客户端解耦。

通过 JSON API 在实践中的解耦

到目前为止,一切顺利。但这种解耦在实践中如何运作?

在我们的文章 拆分你的数据和应用程序 API:更进一步 中,你会找到以下引述:

我工作中最糟糕的部分是这些天为前端开发者设计 API。谈话不可避免地是这样的:

开发者 – 所以,这个屏幕有数据元素 x、y、z……你能创建一个 API,其响应格式为 {x: , y:, z: } 吗?

我 – 好的

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

这个问题的一个潜在解决方案是引入 GraphQL,它允许你拥有更具表现力的 JSON API。这意味着当你的 API 客户端需求变化时,你不需要那么频繁地更改它。

这是一个解决上述问题的合理方法,但它也存在问题。我们看到的最大问题是安全性,正如我们在 API churn/安全性权衡 文章中概述的那样。

显然,Facebook 使用 白名单 来处理 GraphQL 引入的安全问题,但许多使用 GraphQL 的开发者似乎并不理解与之相关的安全威胁。

另一个解决方案:拆分你的应用程序和通用数据 API

Max Chernyak 在他的文章 不要构建通用 API 来驱动你自己的前端 中推荐的另一种方法是构建 两个 JSON API:

这是一个务实的解决方案,用于解决 Web 应用程序前端与支持它的后端代码之间似乎 固有 的耦合,并且它不涉及通用 GraphQL API 的安全权衡。

超媒体 - 网络架构解耦

现在让我们考虑 超媒体 API 如何解耦软件。

考虑对上述 https://example.com/account/12345 的相同 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>
        <a href="/accounts/12345/transfers">transfers</a>
        <a href="/accounts/12345/close-requests">close-requests</a>
    </div>
  <body>
</html>

(是的,这是一个 API 响应。它恰好是一个超媒体格式的响应,在这种情况下是 HTML。)

在这里,我们看到,在应用程序级别,这个响应与“前端”无法更紧密地耦合。事实上,它 就是 前端,在 API 响应不仅指定资源的 data,还提供如何向用户显示此 data 的布局信息的意义上。

响应还包含 超媒体控件,在这种情况下是链接,用户可以从中选择以继续导航此 超媒体驱动应用程序 提供的超媒体 API。

那么,在这种情况下,解耦在哪里?

REST 和一致接口

这种情况下,解耦发生在 更低级别。它发生在 网络架构 级别,即系统级别。超媒体系统 被设计为将超媒体客户端(在 Web 的情况下,是浏览器)与超媒体服务器解耦。

这主要通过 REST 的一致接口约束实现,特别是通过使用 超媒体作为应用程序状态的引擎 (HATEOAS)。

这种解耦风格允许在更高级别的应用程序中更紧密的耦合(我们已经看到这可能是 固有 的耦合),同时仍保留整体系统的解耦益处。

通过超媒体在实践中的解耦

这种解耦在实践中如何运作?好吧,假设我们希望从我们的银行中移除向其他银行转账的能力以及关闭账户的能力。

现在我们的超媒体响应针对此 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-ful 网络架构的核心,特别是 HATEOAS 的核心。

正如你所见,尽管前端和后端之间有更紧密的 应用程序级别 耦合,但由于 REST-ful 超媒体系统 的一致接口方面赋予我们的 网络架构 解耦,我们实际上拥有更多的灵活性。

但那是一个糟糕的(数据)API!

许多人会反对说,当然,这个超媒体 API 对于我们的 Web 应用程序可能很灵活,但它是一个糟糕的通用 API。

这是完全正确的。这个超媒体 API 是为特定 Web 应用程序调整的。尝试下载这个 HTML、解析它并从中提取信息会很繁琐且容易出错。这个超媒体 API 只有作为更大超媒体系统的一部分,被适当的超媒体客户端消费才有意义。

这正是为什么我们在 拆分你的数据和应用程序 API:更进一步 中推荐在你的超媒体 API 旁边创建一个通用 JSON API 的原因。你可以利用超媒体的灵活性来处理你自己的 Web 应用程序,同时为移动应用程序、第三方应用程序等提供通用 JSON API。

(尽管,我们应该提到,一个 基于超媒体的移动应用程序 可能也是一个不错的选择!)

结论

在本文中,我们考察了两种不同类型的解耦:

我们看到,尽管基于超媒体的应用程序中存在更紧密的应用程序级别耦合,但超媒体系统处理变化更优雅。

</>