购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.1 抽象

API使得我们可以在不完全了解所涉及的具体步骤的情况下,也能使用软件。它呈现了一个清晰、可执行的操作列表,从而使其他用户在不一定掌握操作的具体细节的前提下,依然能够有效地执行这些操作。API提供了简化的过程实现。

这些操作有些是纯粹功能性的,输出只与输入有关。例如某个数学函数,在给定行星、恒星的轨道和质量的情况下,计算出它们的重心。

或者,它们也可用于状态处理,因为同样的操作重复两次可能会有不同的结果。例如,检索系统时间的操作。也可以用一个API调用对计算机的时区进行设置,而随后两次API调用进行时间检索,此时也许会返回完全不一样的结果。

在这两种情况下,API都在定义 抽象 的概念。通过一次操作来完成系统时间检索是非常容易的,但也许实现这个操作的细节并不那么简单,它往往涉及以特定方式去读取某些用于跟踪系统时间信息的硬件的操作。

不同的硬件通常会以不同的格式来表示时间,但API的返回结果应始终以标准化的格式呈现,时区和时差也需要进行调整。所有这些复杂的细节问题都是由提供API模块的开发者来处理的,并为所有用户提供一个清晰、可理解的约定。“调用这个函数,将返回ISO(International Organization for Standardization,国际标准化组织)格式的时间”。

虽然这里主要讨论的是API,而且本书将主要描述与在线服务有关的API,但抽象的概念其实可以应用于任何场景。一个管理用户的网页就是一个抽象,因为它定义了“用户账户”的概念和相关参数。另一个无处不在的例子是电子商务中的购物车”。给人以简洁的印象是好的做法,因为它有助于为用户提供一个更清晰、更一致的界面。

当然,这只是一个很简单的例子,但API能将大量的复杂细节隐藏于其接口之下。一个很好的值得研究的范例是像curl这样的程序。即使只是实现向某个URL发送HTTP请求,并输出返回的头部(header)信息这样的功能,也涉及大量复杂的细节问题:

这条命令执行后会访问网站www.google.com,并在执行命令的参数中使用-I以显示HTTP响应的头部信息。添加-L参数是为了自动重定向命令执行过程中的所有HTTP请求。

与服务器建立远程连接涉及多个不同单元的操作:

❍DNS访问,从而将服务器地址www.google.com解析成一个实际的IP地址。

❍同各服务器进行通信,这涉及使用TCP协议来创建一个持久的连接,并保证可靠接收数据。

❍根据第一个请求的结果进行重定向,因为服务器会返回一个指向另一URL的地址。这是通过使用-L参数来实现的。

❍将网站访问请求重定向到一个HTTPS的URL,这个操作需要在之前访问地址的基础上添加一层验证和加密的功能。

上述过程中的每一步也都会调用其他API来执行更细微的操作,其中可能涉及操作系统的功能,或者调用远程服务器的功能(比如通过DNS服务器进行域名解析的操作),以便从其中获取数据。

这里,curl提供的接口是在命令行中使用的。虽然严格意义上的API规定了,其面向的最终用户并非是人类(而是面向计算机),但两者其实并没有什么实质性的区别。好的API应该也是便于人类用户测试使用的。命令行接口还可以很容易地通过使用bash脚本或其他语言来实现操作自动化。

但是,从curl用户的角度来看,这些细节过程并不需要关注。它被简化成通过带有几个参数的一行命令,就可以执行一个含义明确的操作,而不必操心从DNS获取数据的格式,以及如何使用SSL协议对请求过程中的通信数据进行加密等问题。

2.1.1 使用合适的抽象

一个良好的接口,其本质在于创建一系列的抽象,并将其呈现给用户,以便用户能够执行所需的操作。因此,在设计一个新的API接口时,最重要的问题是决定哪些是最合适的抽象。

有组织地进行系统设计时,抽象概念大多是在进行设计的过程中决定的。首先有了基本的设想,确认是对所需解决的问题的正确理解之后,再进行调整。

例如,通过给用户添加不同的参数来启动某个用户管理系统是很常见的情况。这样的话,用户可以拥有执行A操作的权限,还可以通过参数来执行B操作,以此类推。每次执行操作时添加一个参数,乃至可能需要添加十余个参数,这个过程最终会变得非常混乱。

针对这种场景,可以使用新的抽象概念,即角色(role)和权限(permission)。某些类型的用户可以执行不同的操作,比如说具备管理员角色的用户。每个用户都可以拥有一个角色,而该角色在这里就是用于描述其相关权限的。

注意,这样抽象之后能简化前述问题,因为这种方式很容易理解和管理。然而,实现从“一个特定的参数集合”到“几个角色”这种方式的转变,也许会是一个复杂的过程,而且可用的选项组合会有所减少,也许现有的某些用户会用到某个奇特的参数组合。所有这些因素都需要仔细考虑。

在设计一个新的API时,最好尽可能准确地描述API用户所需使用的内部抽象,至少针对高层次抽象来说应当如此。这样做还有一个好处,那就是可以从API用户的角度去考虑问题,看看整个流程能否顺畅运转。

在软件开发人员的工作实践中,最有价值的观点之一,是让自己从“内部视角”中跳出来,站在软件实际使用者的立场上去看待问题。要做到这点比听起来更困难,但这绝对是一个值得去努力掌握的技能。这样将使你成为一个更好的软件系统设计师。不要怕请朋友或同事来检测你的设计中的“盲点”。

然而,每个抽象都有其局限性。

2.1.2 抽象失效

当某个抽象在实现过程中暴露了其细节,并且没有呈现出一个理想的“不透明”的形象时,这种情况就称为抽象失效(leaky abstraction,亦称抽象泄漏)。

虽然一个好的API应当尽量避免出现这种情况,但有时却难以避免。这可能是由实现API的底层代码中的bug造成的,有时也可能归因于代码在某些操作中的实现方式。

在这方面常见的案例是关系数据库。SQL语句是对数据库中进行数据检索的具体实现过程的抽象,可以用复杂的SQL查询进行搜索并得到结果,且无须知道数据在系统中是如何组织的。但是,有时你会发现某个SQL查询操作很慢,并且调整SQL查询的参数会显著影响这种情况的发生,这就是失效的抽象。

这是比较常见的情况,也因此有许多工具用于帮助确定运行SQL查询时背后到底发生了什么,这与API的设计初衷完全是背道而驰的。相关的SQL语句主要是EXPLAIN。

操作系统是一个很好的例子,它会生成适当的抽象,而且在大多数情况下不会失效。有很多相关案例,由于空间不足而无法读写文件(与30年前相比,现在这个问题已经不那么普遍了)、由于网络问题而与远程服务器中断连接,或者由于打开的文件描述符数量达到上限而无法创建新的连接。

从某种程度上来说,抽象失效是不可避免的,这是由于它们并非存在于一个绝对完美的世界。软件难免有bug,理解并为之做好准备至关重要。

“所有非基本抽象,在一定程度上都存在抽象失效”。

——Joel Spolsky的抽象失效定律

在设计API时,出于几个方面的原因,必须考虑到以下情况:

清晰地对外呈现错误和提示 。好的设计都会考虑到出错的情况,并尽可能用对应的错误代码或错误处理来清楚地进行提示。

处理可能来自内部依赖服务的错误 。依赖服务可能会失效或出现其他问题。API应该在一定程度上抽象出这种情况,先尽可能尝试修复问题,如无法修复,则以完善的方式进行故障处理,并返回对应的结果信息。

最好的设计,不仅在于当系统按预期状态工作时能够有效地运转,而且能对意外出现的问题进行预先准备,并确保可以对故障进行分析和纠正。

2.1.3 资源与操作抽象

设计API时值得考虑的一个非常有用的模式,是提出一套可以执行操作(或动作)的资源。这种模式使用两类元素: 资源 操作

资源是被引用的元素,操作则是针对资源所做的事情。

例如,让我们设计一个非常简单的接口,用来玩一个简单的猜硬币游戏。这是一个猜三次硬币投掷结果的游戏,如果猜中两次及以上,则用户获胜。

该游戏的资源和操作可按下表设计:

某次游戏中可能出现的过程输出信息如下:

请注意,每个资源都有一套自己的可执行的操作。如果需要的话,操作可以重复,但并非必需。资源可以被组合成一个层次结构的形式(比如本例中,COIN_TOSS依赖于更高层次的GAME资源)。操作可以要求带参数,这些参数可以是其他资源。

然而,抽象是围绕着有一套具备一致性的资源和操作来组织的。这种明确地组织API的方式非常有用,因为它明确定义了系统中什么是被动的,什么是主动的。

OOP (Object-Oriented Programming,面向对象编程)使用了这些抽象单元,因为所有的一切都是对象,可以通过接收信息来完成某些操作。另一方面,函数式编程并不太适合这种方式,因为“操作”可以像资源一样工作。

这是一种常见的模式,它被用于RESTful接口中,我们接下来将会看到。 Mc1uxWgkIUDSWJzQMwfUBIUweCJBsU8RGMtehu5hoz8mKl0xFHpK0tRjeCCX0yvj

点击中间区域
呼出菜单
上一章
目录
下一章
×