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,然后使用React等JavaScript框架消费该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变更/安全权衡》文章中所概述的。
显然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>账号:12345</div>
<div>余额:$100.00 美元</div>
<div>链接:
<a href="/accounts/12345/deposits">存款</a>
<a href="/accounts/12345/withdrawals">取款</a>
<a href="/accounts/12345/transfers">转账</a>
<a href="/accounts/12345/close-requests">关闭请求</a>
</div>
</body>
</html>
(是的,这是一个API响应。它只是恰好是超媒体格式的响应,在这个案例中是HTML。)
在这里我们看到,在应用级别,这个响应与"前端"的耦合不能再紧密了。事实上,它就是前端,因为API响应不仅指定了资源的数据,还提供了如何向用户显示这些数据的布局信息。
响应还包含超媒体控件,在这个案例中是链接,终端用户可以选择它们继续浏览这个超媒体驱动应用提供的超媒体API。
那么,在这种情况下解耦体现在哪里呢?
这种解耦发生在较低级别。它发生在网络架构级别,也就是说,在系统级别。超媒体系统旨在将超媒体客户端(在Web案例中是浏览器)与超媒体服务器解耦。
这主要通过REST的统一接口约束实现,特别是通过使用超媒体作为应用程序状态引擎(HATEOAS)。
这种解耦风格允许在更高级别的应用级别进行更紧密的耦合(我们已经看到这可能是固有的耦合),同时仍然为整个系统保留解耦的好处。
这种解耦在实践中如何运作?假设我们希望移除从银行向其他银行转账以及关闭账户的能力。
现在我们对这个GET
请求的超媒体响应是什么样子的?
HTTP/1.1 200 OK
<html>
<body>
<div>账号:12345</div>
<div>余额:$100.00 美元</div>
<div>链接:
<a href="/accounts/12345/deposits">存款</a>
<a href="/accounts/12345/withdrawals">取款</a>
</div>
</body>
</html>
你可以看到在这个响应中,那两个操作的链接已经从HTML中移除了。浏览器只是向用户渲染新的HTML。几乎没有客户端还在使用旧的API。API被编码在超媒体中并通过超媒体发现。
这意味着我们可以在不破坏客户端的情况下大幅更改API。
这种灵活性是REST-ful网络架构的核心,特别是HATEOAS的核心。
如你所见,尽管前端和后端在应用级别的耦合更紧密,但由于REST-ful 超媒体系统的统一接口方面为我们提供了网络架构解耦,我们实际上拥有更大的灵活性。
许多人会反对说,当然,这个超媒体API可能对我们的Web应用程序很灵活,但它作为一个通用API来说很糟糕。
这非常正确。这个超媒体API是为特定Web应用程序调整的。下载这个HTML,解析它并尝试从中提取信息将是繁琐且容易出错的。这个超媒体API只有在作为更大的超媒体系统的一部分,被适当的超媒体客户端消费时才有意义。
这正是为什么我们在《拆分你的数据与应用API:更进一步》中建议在超媒体API旁边创建一个通用JSON API。你可以为自己的Web应用程序利用超媒体的灵活性,同时为移动应用程序、第三方应用程序等提供通用JSON API。
(不过我们应该提到,基于超媒体的移动应用程序可能也是个不错的选择!)
在本文中,我们研究了两种不同类型的解耦:
我们发现,尽管基于超媒体的应用程序在应用级别有更紧密的耦合,但超媒体系统能更优雅地处理变更。