第一章 什么是API
本章涵盖:
- 什么是接口
- 什么是API
- 什么是资源导向
- 什么是“好”的API
很可能,拿起这本书的人已经对API的上层概念有所了解。此外,你可能已经知道API代表应用程序编程接口,因此本章的重点将更详细地解释这些基础知识,以及它们为什么重要。让我们更仔细地看看API的这个概念。
1.1 什么是web API
API(应用程序编程接口)定义了计算机系统相互交互的方式。鉴于不可能有系统存在于真空中,API无处不在也就不足为奇了。我们可以在我们使用的语言包管理器中找到API(例如,提供类似function encrypt(input: string): string
的加密库)。在我们自己编写的代码中也可以找到API,即使他们不是为他人使用而产生。但有一种特殊类型的API是专为通过网络公开并远程为他人使用的,这正是本书的重点,通常被称为“Web API”。
Web API在许多方面都很有趣,但这个特殊类别最有趣的方面可能是建立API的人有很大的控制权,而使用Web API的人相对较少。当我们使用库时,我们处理库本身的本地副本,这意味着构建API的人可以随心所欲地做任何他们想做的事情,而不会伤害用户。Web API则不同,因为没有副本。相反,当Web API的构建者进行更改时,这些更改会被强制应用于用户,无论更改是否源自用户要求。
例如,想象一下一个允许你加密数据的Web API调用。如果负责这个API的团队决定在加密数据时使用不同的算法,你实际上没有选择的余地。在调用加密方法时,你的数据将使用最新的算法进行加密。在一个更极端的例子中,团队可以决定完全关闭API并忽略你的请求。那时,你的应用程序将突然停止工作,你无法做太多事情。图1.1展示了这两种情况。
图1.1 处理Web API时可能出现的面向消费者的体验
然而,对于API的使用者来说,Web API的缺点通常是那些构建API的人的主要优势:他们能够完全掌控API。例如,如果加密API使用了一个绝密的新算法,构建它的团队可能不希望将那段代码以库的形式随意分享给全世界。相反,他们可能更喜欢使用Web API,这将允许他们展示超级秘密算法的功能,而不会泄露他们宝贵的知识产权。还有一些情况,某个系统可能需要非凡的计算能力,如果部署为库并在家用计算机或笔记本电脑上运行,那么运行时间将太长。在这些情况下,例如在许多机器学习API中,构建Web API允许你展示强大的功能,同时将计算需求对使用者隐藏起来,如图1.2所示。
图1.2 一个隐藏所需计算能力的Web API的示例
既然我们了解了API(特别是Web API)是什么,这就引发了一个问题:它们为什么如此重要呢?
1.2 为什么API很重要
虽然将软件设计和构建仅供人类使用并不罕见,这本身并没有根本性问题。然而,过去几年,我们越来越多地关注自动化,我们的目标是构建能够以更快的速度执行人类工作的计算机程序。不幸的是,正是在这一点上,“仅供人类使用”的软件成为了一个问题。
当我们专门为人类使用设计某物,其中我们的互动涉及鼠标和键盘时,我们倾向于将系统的布局和视觉方面与原始数据和功能方面混为一谈。这是有问题的,因为很难向计算机解释如何与图形界面进行交互。而且这个问题变得更加严重,因为改变程序的视觉方面也可能要求我们重新教导计算机如何与这个新的图形界面进行交互。实际上,虽然对我们来说这些变化可能只是表面上的,但对计算机来说却是完全无法识别的。换句话说,对于计算机来说,不存在“仅仅是表面上的”这种东西。
API是专门为计算机设计的接口,具有使计算机能够轻松使用它们的重要特性。例如,这些接口没有视觉方面的内容,因此无需担心表面上的变化。而且这些接口通常以“兼容”(compatible)的方式演进(参见第24章),因此在面对新的变化时不需要重新教导计算机任何内容。简而言之,API提供了一种以安全和稳定的方式与计算机进行交互所需的语言。
但这并不仅限于简单的自动化。API还打开了组合(composite)的大门,使我们能够像乐高积木一样处理功能,将各个部分组合在一起,以新颖的方式构建远大于其各部分之和的东西。为了完成这个循环,这些新的API组合也可以加入可重复使用的构建块的行列,从而实现更复杂和非凡的未来项目。
但这引出了一个重要问题:我们如何确保我们构建的API像乐高积木一样相互契合?让我们首先看看其中一种策略,即资源导向(resource orientation)。
1.3 什么是资源导向API
许多现今存在的Web API有点像仆人:你要求它们做某事,它们就会去做。例如,如果我们想要获取我们家乡的天气,我们可以像对待仆人一样命令Web API来predictWeather(postalCode=10011)
。通过调用预配置的子程序或方法来命令另一台计算机执行某项任务的这种方式通常被称为进行“远程过程调用”(RPC),因为我们实际上是在调用一个库函数(或过程),以在潜在遥远的另一台计算机上执行(或远程执行)。像这样的API主要关注正在执行的操作。也就是说,我们考虑计算天气(predictWeather(postalCode=...)
)或加密数据(encrypt(data=...)
)或发送电子邮件(sendEmail(to=...)
)等,每个都强调“正在做”某事。
那么为什么不是所有API都是RPC导向的呢?其中一个主要原因与“有状态性”(statefulness)的概念有关,因为API调用可以是“有状态的”或“无状态的”。当API调用可以独立于所有其他API请求而进行,而且没有任何额外的上下文或数据时,被认为是无状态的。例如,用于预测天气的Web API调用只涉及一个独立的输入(邮政编码),因此被视为无状态。另一方面,一个Web API可以存储用户喜欢的城市并提供这些城市的天气预报,这个API在运行时不需要输入,但需要用户已经存储了他们感兴趣的城市。因此,这种涉及其他先前请求或先前存储的数据的API请求被视为有状态。事实证明,RPC风格的API非常适用于无状态功能,但当我们引入有状态的API方法时,它们往往不太合适。
注意:如果你熟悉REST,现在可能是一个好时机指出,这一节不是关于REST和RESTful API特定的,而更普遍地涉及强调“资源”的API(正如大多数RESTful API所做的)。换句话说,虽然与REST的主题有很多重叠,但这一节比仅仅是REST要更通用一些。
为了理解这一点,让我们来看一个用于预订航班的有状态API的示例。在表1.1中,我们可以看到一系列用于与航空旅行计划进行交互的RPC,涵盖了安排新预订、查看现有预订和取消不需要的旅行等操作。
表1.1 示例航班预订API方法摘要
方法 | 描述 |
---|---|
ScheduleFlight() | 预定新航班 |
GetFlightDetails() | 展示特定航班详细信息 |
ShowAllBookings() | 展示当前所有预定信息 |
CancelReservation() | 取消预定 |
RescheduleFlight() | 更改已预定航班 |
UpgradeTrip() | 升级舱位 |
这些RPC方法都相当具有描述性,但我们无法避免需要记住这些API方法,每个方法都与其他方法略有不同。例如,有时候一个方法涉及“航班”(例如,RescheduleFlight()
),而其他时候操作“预订”(例如,CancelReservation()
)。我们还需要记住使用了多少个同义词形式的操作。例如,我们需要记住如何查看所有的预订,是使用ShowFlights()
、ShowAllFlights()
、ListFlights()
还是ListAllFlights()
(在本例中应该使用ShowAllFlights())。但是,我们该如何解决这个问题呢?答案在于标准化。
资源导向(resource orientation)旨在通过为API设计提供两个方面的标准化模块来帮助解决这个问题。首先,资源导向API依赖于“资源”的概念,这些资源是我们存储和互动的关键概念,标准化了API管理的“事物”。其次,与使用任意RPC名称执行我们能想到的任何操作不同,资源导向API将操作限制为一小组标准操作(见表1.2),这些操作适用于每个资源,以形成API中的有用操作。从稍微不同的角度来看,资源导向API实际上只是RPC式API的一种特殊类型,其中每个RPC都遵循清晰和标准化的模式:<StandardMethod><Resource>()
。
表1.2 标准方法及其含义
RPC | 描述 |
---|---|
Create | 创建新资源 |
Get | 获取并展示特定资源信息 |
List | 获取并展示当前所有资源列表 |
Delete | 删除一个资源 |
Update | 更新一个资源 |
如果我们选择采用这种特殊且有限的RPC方法,那意味着与表1.1中所示的各种不同RPC方法相比,我们可以创建一个单一的资源(例如,FlightReservation
),并使用表1.3中所示的一组标准方法获得等效的功能。
表1.3 标准方法应用于“航班”资源
标准方法 | 资源 | 方法名 | ||
---|---|---|---|---|
Create | X | FlightReservation | = | CreateFlightReservation() |
Get | X | FlightReservation | = | GetFlightReservation() |
List | X | FlightReservation | = | ListFlightReservation() |
Delete | X | FlightReservation | = | DeleteFlightReservation() |
Update | X | FlightReservation | = | UpdateFlightReservation() |
标准化显然更有组织,但这是否意味着所有的面向资源的API都严格优于面向RPC的API?实际上并非如此。对于某些情况,面向RPC的API可能更适合(特别是在API方法是无状态的情况下)。然而,在许多其他情况下,面向资源的API对于用户来说会更容易学习、理解和记住。这是因为资源导向API提供的标准化使您可以轻松地将已知的东西(例如,一组标准方法)与您可以轻松学习的东西(例如,新资源的名称)相结合,从而立即开始与API互动。更具数值意义的说法是,如果您熟悉,比如,五种标准方法,那么由于可靠模式的力量,学习一个新资源实际上等同于学习五个新的RPC方法。
显然,重要的是要指出并不是每个API都相同,以“要学习东西的多少”来定义API的复杂性有点粗糙。另一方面,这里有一个重要的原则在起作用:模式的力量(the power of patterns)。通常,学习可组合的模块并将它们组合成遵循一种固定模式的更复杂的事物,比学习每次都遵循自定义设计的复杂事物更容易。由于资源导向API利用经过验证的设计模式的强大力量,它们通常更容易学习,因此比其面向RPC的等效物“更好”。但这引出了一个重要问题:这里的“更好”是什么意思?我们如何知道一个API是“好”的?甚至“好”意味着什么?
1.4 什么是“好”的API
在我们探讨使API变得更“好”之前,首先需要深入了解为什么我们需要一个API。换句话说,建立API的目的是什么?通常,这可以归结为两个简单的原因:
- 我们拥有一些用户想要使用的功能。
- 这些用户希望以编程方式使用这些功能。
举个例子,我们可能拥有一套出色的系统,可以将文本从一种语言翻译成另一种语言。世界上可能有很多人想要拥有这种能力,但光有这一点还不够。毕竟,我们可以推出一个翻译手机应用程序而不是一个API。所以,那些想要这个功能的人必须还想编写一个使用它的程序,我们才有必要建立一个API。有了这两个标准,我们再来思考一个好的API应该具有什么特质呢?
1.4.1 操作性
首先最重要的部分是,无论最终的界面是什么样的,整个系统必须是可操作的。换句话说,它必须执行用户实际想要的功能。如果这是一个将文本从一种语言翻译成另一种语言的系统,那么它必须确实能够执行这个功能。此外,大多数系统很可能会有许多非操作性要求(nonoptional)。例如,如果我们的系统将文本从一种语言翻译成另一种语言,可能会有与延迟(例如,翻译任务应该花费几毫秒,而不是几天)或准确性(例如,翻译不应误导)等非操作性要求相关的事项。我们说,这两个方面共同构成了系统的操作性方面。
1.4.2 表达性
如果一个系统具备某种功能很重要,那么同样重要的是,该系统的接口允许用户清晰而简单地表达他们想要做的事情。换句话说,如果系统将文本从一种语言翻译成另一种语言,那么API应该设计得如此清晰和简单,以便有一种明确的方式来实现这一目标。在这种情况下,可能会有一个名为TranslateText()
的RPC。这种事情可能听起来很明显,但实际上可能比看起来更复杂。
比如,一个API已经支持某些功能,但由于我们的疏忽,我们没有意识到用户需要这些功能,因此没有构建好的表达方式使用户能够轻松访问该功能。这种情况通常表现为用户会采取一些变通方法来访问已经支持的隐藏功能。例如,如果一个API提供将文本从一种语言翻译成另一种语言的能力,那么可能有用户会迫使API充当语言检测器,即使他们实际上并不真正想翻译任何东西。如清单代码1.1所示。正如你所想的,如果用户有一个名为DetectLanguage()
的RPC会更好。
代码1.1 使用 TranslateText API 方法来实现检测语言的功能
function detectLanguage(inputText: string): string {
const supportedLanguages: string[] = ['en', 'es', ... ];
for (let language of supportedLanguages) {
// 这里假定API定义了一个`TranslateText`方法,该方法接受输入文本和目标语言以进行翻译。
let translatedText = TranslateApi.TranslateText({
text: inputText,
targetLanguage: language
});
// 如果翻译后的文本与输入文本相同,那么我们知道这两种语言是相同的。
if (translatedText == inputText) {
return language;
}
}
// 如果我们找不到与输入文本相同的翻译文本,我们将返回null,表示我们无法检测输入文本的语言。
return null;
}
正如这个例子所示,支持某些功能但不让用户轻松访问这些功能的API不太好。另一方面,富有表现力的API允许用户清晰地表达他们想要什么(例如,翻译文本)甚至如何完成任务(例如,“在150毫秒内完成,准确率达到95%”)。
1.4.3 简洁性
与任何系统的可用性相关的最重要的事情之一是简洁性。有时候人们会认为简洁是减少API中的事物(例如,RPC、资源等)的数量,但不幸的是,这种情况很少发生。例如,一个API可以依赖于一个处理所有功能的单一ExecuteAction()
方法;然而,这实际上并没有简化任何东西。相反,它将复杂性从一个地方(大量不同的RPC)转移到另一个地方(单个RPC中的大量配置)。那么一个简洁的API究竟是什么样的呢?
与其试图过度减少RPC的数量,API应该致力于以尽可能简单的方式公开用户所需的功能,使API尽可能简洁但不过于简洁。例如,想象一下一个翻译API希望添加检测输入文本语言的功能。我们可以通过在翻译响应中返回检测到的源文本来实现这一点;然而,这仍然是一种功能混淆,因为该功能隐藏在设计用于其他目的的方法中。相反,更合理的做法是创建一个专门用于此目的的新方法,例如DetectLanguage()
。(请注意,我们可能还会在翻译内容时返回检测到的语言,但这完全是为了另一个目的。)
关于简洁性的另一个方面关乎“常见情况”。我们将更多精力放在关注可用性上,同时为特殊情况留出余地。其目的是“让常见情况变得出色,让高级情况成为可能”。这意味着每当您添加可能会使API变得复杂以造福高级用户的功能时,最好将此复杂性对典型用户(只对常见情况感兴趣)进行充分隐藏。这样,更频繁的情景变得简单和容易,同时仍然为那些高级功能提供支持。
例如,假设我们的翻译API包括一个用于翻译文本的机器学习模型的概念,其中我们不是指定目标语言,而是选择一个基于目标语言的模型,并将该模型用作“翻译引擎”。尽管这种功能为用户提供了更大的灵活性,但它也更复杂,新的常见情况如图1.3所示。
图1.3 通过选择匹配模型进行文本翻译
正如我们所看到的,为了支持更高级的功能,我们实际上使翻译某些文本变得更加困难。为了更清楚地看到这一点,对比代码1.2中显示的代码与简单调用TranslateText("Hello world", "es")
的简洁性。
代码1.2 通过选择匹配模型进行文本翻译
function translateText(inputText: string, targetLanguage: string): string {
// 由于我们需要选择一个模型,因此首先需要知道输入文本的语言
// 为了确定这一点,我们可以依赖API提供的假设的`DetectLanguage()`方法。
let sourceLanguage = TranslateAPI.DetectLanguage(inputText);
// 一旦确定了输入和输出语言,我们可以选择任何API提供的与之匹配的模型。
let model = TranslateApi.ListModels({
filter: `sourceLanguage:${sourceLanguage}
targetLanguage:${targetLanguage}`,
})[0];
// 至此我们获取了需要的输入参数,可以调用对应方法进行翻译了。
return TranslateApi.TranslateText({
text: inputText,
modelId: model.id
});
}
我们应当如何设计这个API,以使其尽可能简洁但不过于简单,且使常见情况变得出色,同时使高级情况成为可能呢?由于常见情况涉及那些实际上并不关心特定模型的用户,我们可以通过设计API,使其接受targetLanguage
或modelId
中的任一个。高级情况仍然可以工作(实际上,清单1.2中显示的代码仍将有效),但常见情况将显得简单得多,仅依赖于targetLanguage
参数(并使modelId
参数保持未定义)。
代码1.3 文本翻译(常见情况)
function translateText(inputText: string,
targetLanguage: string,
modelId?: string): string {
return TranslateApi.TranslateText({
text: inputText,
targetLanguage: targetLanguage,
modelId: modelId,
});
}
现在我们对"好"的API的简洁性属性有了一些了解,让我们来看最后一点:可预测性。
1.4.4 可预测性
虽然生活中的惊喜有时可能很有趣,但API中不应该出现惊喜,无论是在接口定义还是底层行为。这有点像有关投资的老话:“如果令人兴奋,那么你做错了。”那么我们所说的“不令人惊讶”的API是什么意思呢?
不令人惊讶的API依赖于在API表面定义和行为上应用的重复模式。例如,如果一个翻译文本的API具有一个TranslateText()
方法,该方法将输入内容作为名为text
的字段传递,那么当我们添加DetectLanguage()
方法时,输入内容也应该称为text
(而不是inputText
、content
或textContent
)。虽然现在这可能看起来很明显,但请记住,许多API是由多个团队构建的,当提供一组选项时,字段命名的选择通常是任意的。这意味着当两个不同的人负责这两个不同的字段时,他们很可能会做出不同的任意选择。当发生这种情况时,我们最终得到一个不一致(因此令人惊讶)的API。
尽管这种不一致性可能看似微不足道,但事实证明,这些问题比它们看起来更重要。这是因为事实上,API的用户很少通过彻底阅读所有API文档来学习每一个细节。相反,用户只会阅读适当的文档来完成他们想要做的事情。这意味着如果某人学到一个请求消息中的字段称为text
,那么他们几乎可以假定在另一个请求中它也是以相同的方式命名的,从而在他们已经学到的基础上对他们尚未学到的事情进行有根据的猜测。如果这个猜测失败(例如,因为另一条消息将该字段命名为inputText
),他们的生产力就会受到阻碍,他们不得不停下来弄清楚为什么他们的假设失败了。
显而易见的结论是,依赖重复、可预测模式(例如,一致性地命名字段)的API更容易、更快地学习,因此更好。更复杂的模式,如我们在探讨资源导向API时看到的标准操作,也会带来类似的好处。这使我们进入了本书的整个目的:使用众所周知、明确定义、清晰(希望如此)的模式构建的API将导致可预测且易于学习的API,这会构造出总体上更“好”的API。现在,我们对API以及使其变得更好的因素有了很好的了解,让我们开始考虑在设计API时可以遵循的更高级模式。
本章总结
- 接口是定义两个系统应该如何相互交互的合约。
- API是接口的一种特殊类型,它定义了两个计算机系统如何相互交互;其有多种形式,如可下载的库和Web API。
- Web API是特殊的,因为它们在网络上暴露功能,隐藏了实现细节或功能所需的特定计算要求。
- 资源导向API是通过依赖一组标准操作(称为方法)跨有限的事物(称为资源)来减少复杂性的一种API设计方式。
- 使API“好”的因素有点模糊,但一般来说,好的API具有操作性、表达性、简洁性和可预测性。