第一章 简介
1.1 应用程序编程接口是什么?
应用程序编程接口(API)提供对问题的一个抽象,并说明客户端如何与实现这个问题的解决方案的软件组件来进行交互。 这些组件本身通常作为一个软件库发布,允许多个程序来使用它们。从本质上讲,API定义了可重用的构建模块,并允许功能模块被集成到最终用户的程序中。
API可以为你自己而写,也可以是为你所在组织的其他工程师而写,或者为了更大范围的整个开发社区而写。它可以小到只有一个函数,或者大到调用数百个类、方法、释放函数、数据类型、枚举和常量。它的实现可以是专有的或者开源的。最重要的基本概念是,API是定义良好的接口,为其他的软件提供了一个特定的服务。
现代软件通常构建在众多API之上,其中一些API还依赖更底层的API。请参照图示1.1,这个示例程序直接依赖三个API库,其中有两个API依赖更底层的两个库(4和5)。例如,一个图像浏览程序可能使用某个API来装载GIF图像,而这个API本身是建立在一个底层的压缩和解压缩数据的API之上。
现代软件开发中的API开发是比较普遍的。其目的是为组件提供一个功能上的逻辑接口,并同时隐藏所有的实现细节。例如,装载GIF图像的API可以仅仅提供一个LoadImage()方法来接收文件名并返回一个2D像素数组。所有的文件格式和数据压缩的细节都被这个简单的接口所隐藏了。这个概念也在图示1.1阐明了,客户端代码只能通过公开的接口来访问这个API,请参见每个盒子上面的深色区域。
1.1.1 契约和承包商
打个比方,你有一个给自己盖房子的任务。假设你是完全靠自己来建房子,你就需要具备建筑、水电、木工、泥瓦工和许多其他行业的专业知识。你还要亲自来执行各项任务,记录项目的每个细节。例如,地板需要的木头是否足够,或者是否有和螺丝配套的螺帽。最后,因为整个项目就你一个人在做,所以在任何时间点都只能执行单个任务。因此完成整个项目将消耗你大量时间。
图1.1
上图程序是一个从API层次调用的例程。每个盒子代表一个软件库。深色区域表示公共接口或者说是这个库的API,而白色区域表示API中被隐藏的实现部分。
还有一种办法是聘请专业的承包商来为你执行关键任务。你可以聘请一位建筑师为房子设计一份蓝图,请木匠负责所有木工,请管道工为房子安装水管和排污系统,而让电工安装电力系统。采用这种方法,你得和每个承包商协商,告诉他们要完成什么工作并谈妥价格,接着他们就开始为你工作。如果你比较幸运,有个好朋友是一名承包商,他免费为你工作。通过这种策略,你并不需要知道盖房子的方方面面的细节。你是一个更高级别的管理角色,你的目的是选择最好的承包商,并确保每一个承包者可以团结合作,为你建一个理想的家。
上述内容对API的比喻是显而易见的:你在建设的房子相当于你要写的一个软件程序,承包商是为你提供需要执行的每个任务的抽象和隐藏任务执行细节的API。你的任务是为程序选择合适的API并集成到你的软件中。那个为你免费工作的好朋友承包商则比喻有个熟练的朋友为你提供免费服务,这相当于可以免费使用的开源库。相比之下,你要在软件中使用商业库的话则要支付许可费。这个比喻还可以扩展开来,比如一个承包商又雇用了另一个承包商,这相当于一个API要依赖另一个API来执行它们的任务。
用承包商做比喻在面向对象编程中较为常见。这个领域的前辈讲过一种对象定义了一个为服务或者行为所绑定的契约。该对象当被一个客户端程序请求时执行那些服务,在幕后把一些工作分包给其他对象。 (Meyer, 1987; Snyder, 1986).
[图1.2]
通过承包商执行专门的任务来建设一所房子
1.1.2 C++中的各种API
严格来说,API只是描述了如何与组件交互。也就是说,它提供了一个抽象和组件的功能规范。事实上,许多软件工程师更喜欢把API解释成抽象编程接口(Abstract Programming Interface),而不是应用程序编程接口。(Application Programming Interface)
在C++中,API由一个或多个头文件(.h)外加支持文档组成。对于一个给定的API实现,往往表现为一个可以链接到最终用户程序的库文件。这可以是一个静态库,例如Windows系统中.lib文件,或者是Mac OS X系统和Linux系统中的.a文件,也可以是一个动态库,例如Windows系统中的.dll文件,Mac系统的.dylib或者Linux中的.so。
一个C++API通常包含以下几种元素:
(1).头部(Headers):头文件的集合是定义接口和允许针对这个接口编译客户端代码。开源API还包含了该API实现的源代码(.cpp文件)。
(2).库:若干个静态或动态库文件,为API提供一个实现。客户端可以把这些库文件嵌入到它们的代码中以添加功能到它们的应用程序。
(3).文档:概要信息描述了如何使用API,通常包括API中的所有类和函数自动生成的文档。
举个例子,众所周知的API,微软的Windows API(通常也称为Win32 API)是一个C函数、数据类型和常量的集合,使程序员可以编写在Windows平台运行的程序。这包含了文件处理、进程和线程管理、创建图形用户界面和网络通讯等。
Win32 API是一个纯C编写的 API例子,而不是C++ API。你可以在C++程序中直接使用一个由C语言编写的 API,标准模板库(STL)是一个特殊的C++ API的好例子。STL包含容器类、在这些容器内定位元素的迭代器和各种容器内的算法(Josuttis,1999)。例如,这些算法包括高级操作 std::search(), std::reverse(), std::sort(), 和 std::set_intersection()(搜索、反转、排序和设置交集)。STL针对特定任务提供了一个逻辑接口来操作元素集,而不必对外暴露每个算法实现的内部细节。
建议
API就是软件组件的一个逻辑接口,它隐藏了实现它的内部细节。
1.2关于API设计的差异
接口是由开发人员编写的最为重要的代码。这是因为,修复接口中问题的代价比修复相关联的实现代码中问题的代价要高很多。因此,比起普通程序开发或图形用户界面(GUI)开发,开发可共享的API需要更加注意。当然,开发这些都需要实践经验。尽管如此,就API开发来说,这些都是成功的关键。具体来说,API开发的一些关键差异因素体现在如下几个方面:
API是为开发人员而设计的一种接口,这非常像GUI是为最终用户设计的接口。有种说法是API是为程序员设计的用户接口(Arnold, 2005)。因此,你设计的API可能会被全世界数以千计的程序员使用,而且你也无从知道他们用在什么地方(Tulach, 2008)。你要在设计中明白这一点。设计良好的API可以成为你所在组织最大的资产。相反地,一个设计差劲的API如同恶梦一般,甚至会让你的用户选择你的竞争对手(Bloch, 2005),如同一个常出问题或者难用的GUI(图形用户界面)都可能会让用户*转向其他程序。
多个应用程序可以共享同一个API。图1.1显示单个程序可以由多个API组成。不过,这些API中的任何一个也可以再被其他若干个程序所使用。这意味着,任何一个程序自己里面的代码出问题只会影响这个程序本身,而如果一个API出错则会影响所有依赖这个API的程序。
无论什么时候,你要修改API的话,都要尽量做到向后兼容。如果你对接口做了一个不兼容的修改,客户端代码就会编译失败,或者破坏代码以致运行结果出现变化或者间歇性崩溃。可以想象一下,假设标准C库的printf()函数签名,因编译器或者平台的不同而导致不同,会导致很大的混乱。这样的话,简单的“Hello World” 程序将不再简单:
[P5 代码1]
这个看起来像人为的例子,但它实际上并不极端。你可以看看你所用的编译器自带的标准头文件,其声明难以理解,可能比上面例子还难。
由于向后兼容的需求,关键是要有一个适当地变更控制过程。在普通开发过程中,许多开发人员可以对API进行错误修复或添加新功能。这些开发人员中的一部分可能是初级工程师,并不精通API设计的所有方面。因此,在发布API的新版本之前应该举行一次API评审。评审应该由一名或多名资深工程师参加,以确保对接口的所有变更都是可行的、都有正当的理由,而且能够最大限度地保证向后兼容。许多开源API的一个改动在添加到源代码之前,必须通过批准。
API的存在时间会很长。设计良好API的前期投入非常巨大,因为需要额外的规划、设计、版本维护以及评审等必要的环节。然而,如果做得好,长期成本可以大大降低,因为你可以在不影响客户的前提下彻底修改或改进你的软件。也就是说,API为你提供的灵活性可以使你的开发速度大为提高。
当编写API时,需要一份好的说明文档,特别在你并没有公开你的实现源码时。 用户是可以通过查看头文件来知道如何使用API(但这样做比较慢而且有难度),而且这也并没有定义API的相关行为,例如可接受的输入值或错误条件。编写良好、连贯且详细的文档是任何一个出众的API所必须的。
同样,自动化测试也是十分重要的。当然,你总是可以测试自己的代码,不过当你给数百个其他开发人员(而开发人员又给数以千计的用户编写程序)编写API时,他们都十分依赖你代码的准确度。假设你对API的实现做了重大调整,而你有一套完整的回归测试程序来确保期望的API功能并没有改变,这样的话你就可以更有信心来确保不会影响你的用户的程序了。
编写一个出众的API是不容易的。除了需要软件设计模式中的必要技能外,还需要额外的知识和解决刚刚列出的那些问题。然而,工程师们很难遇到有人教授API设计的原理和技术。通常,这些技能只能通过经验来获得——通过犯错并获得哪些可以做,哪些不能做的经验(Henning, 2009)。本书将努力解决这个问题,提取有效、不易过时的API设计的精华,这些是通过多年的软件工程经验总结出来的全面的、通过实践的和可行的设计。
提示
API是用来给其他工程师构建他们的程序。因此,它必须设计良好、文档化、通过回归测试和安全稳定。
1.3为什么要使用API?
在你的软件项目中,为什么要关注API,这个问题可以从两个角度来解读: (1) 为什么你应该自行设计和编写API?(2) 为什么你要在程序中使用他人提供的API?接下来的几节会一边列举在你的项目中使用API的各种好处,一边从这两个角度来回答这个问题。
1.3.1更稳定的代码
如果你要编写一个供其他开发人员使用的模块,可以是你所在组织的同事或者外部客户,那么给他们写一套能够访问你的功能的API是非常明智的选择。这么做会给你带来以下好处:
隐藏实现:隐藏模块的实现细节可以得到灵活性,让你能在将来某一天修改实现而不会给用户带来麻烦。如果不这么做的话,你将只能限制对你的代码进行修改或者强制你的用户必须修改他们的代码来适应新版本的库。如果用户升级到新版本太过麻烦,他们很可能根本就不去更新或者换个维护起来轻松的API。一个好的API设计对你的业务或者项目的成功是非常重要的。
提高生存周期: 随着时间的推移,对外暴露执行细节的系统倾向形成大量琐碎的代码,而系统的每个部分又依赖于系统其它部分的内部细节。因此,系统将变得脆弱、僵化、不易更改和高粘度((Martin, 2000)。这经常导致必须花费大量时间来组织,才能让代码变得更好或得从头开始重写。通过预先设计好的API和支付增量成本以维持一个统一的设计,你的软件将可以存活的更久和减少维护成本。我将会在第四章开头更深入地讲解这些。
促进模块化:API通常是设计用来解决特定任务或用例的。例如,API倾向于定义一个拥有紧密焦点的功能模块分组。开发一个基于API集合的程序将促进松耦合和模块化结构,一个模块的行为并不依赖另一个模块的内部细节。
减少冗余代码:代码冗余是软件工程师的一个主要过失之一,只要可能的话无论何时都要杜绝。你应该将所有客户端必须使用的代码逻辑都隐藏在一个严格的接口之内,把行为集中到单独的一个地方。这样做意味着你只要更新一个地方,就可以为你所有的客户改变API的行为。在你的全部代码库中,这样可以移除实现代码的冗余部分。事实上,很多API是在创建之后才发现冗余代码的并决定把它隐藏到单一接口之内。这样做是很好的。
移除硬编码设定:许多程序都包含硬编码值,并在全部代码中有多处拷贝。例如,当有数据写到日志文件的时候,使用myprogram.log文件。API可以用来访问这些信息而不需要从代码库中拷贝这些常量值。举个例子,调用GetLogFilename() API可以用来代替"myprogram.log" 字符串这个硬编码。
更易修改实现: 如果你已经把模块中的所有实现细节都隐藏在它的一个公共接口之内,那么你就能够修改实现细节而不影响依赖于API上的任何代码。例如,你决定修改一个文件解析例程,使用std::string方法来代替需要分配、释放和重新分配的char *缓冲的方法。
更易优化:同样,当你设计的API实现细节隐藏地很成功时,你可以在不需要修改任何客户端代码的情况下优化你的API性能。例如,你可以给一个计算量很大的方法添加一个缓存解决方案。这是可行的,因为所有底层数据的读写都是通过API执行的,因此将很容易知道何时废弃缓存结果并重新计算以得到一个新值。
1.3.2代码重用
代码重用就是利用现有的代码去构建新的软件。这是现代软件发展的一个重大成就之一。API为实现代码重用提供了一种机制。
在早起的软件开发中,下面的事情是比较普遍的,某个公司不得不为自己制作的程序编写所有的代码。如果一个程序需要读取GIF图片或者解析一个文本文档,该公司就得编写所有代码。现在,得益于商业化和开源库,利用他人编写的代码就可以非常容易的重用代码。例如,有非常多的API用来实现读取开源图像和解析XML,这些你都可以下载下来并应用到你的程序中。这些库都经过全球许多开发人员的千锤百炼,并在很多程序中实践过了。
本质上,软件开发变得越来越模块化,通过各种组件的使用来构建程序的应用模块,并通过他们发布的API来进行模块间的通信。这种做法的好处就是你不需要精通每个软件组件的细节,这和先前提过的建造房屋的比喻是类似的,你可以把很多细节都委托给专业的承包商。这样做可以加速软件开发周期,这是通过代码重用或为各种组件降低时间的耦合性来实现。这样也就允许你把精力放在构建你自己的核心业务逻辑上,而不是在其他事情上浪费时间。
实现代码重用的一个困难就是,你常常需要设计比你原来计划更多的通用接口。这是因为其他客户可能提出新的期望和需求。有效率的代码重用需要对客户需求和如何把客户的想法集成到自己的系统中拥有较深的理解。
C++API和网络
在云计算领域,程序依赖第三方API的趋势是很明显的。网络程序越来越依赖Web Service(网络服务 也是各种API)来提供核心功能。在Web mashups(Web 2.0下的一种交互式Web应用程序)这个例子中,有时程序本身由多个已知服务打包成一个新的服务。例如,可以把组合本地犯罪统计数据库的Google 地图API提供给一个基于地图的接口。
事实上,花时间留意C++ API设计在Web开发中的应用是值得的。一个浅显的分析会得出这样的结论:服务器端Web开发被限制在脚本语言里,例如PHP、Perl、Python或者基于微软ASP技术的.NET语言等。这么说在狭义的Web开发中是成立的。然而,值得注意的是,很多大规模的Web 服务是使用C++开发的后台以优化性能的。
事实上,Facebook 开发了一个叫HipHop的产品,可以把PHP代码转换成C++以提高社交网络网站的性能。因此,C++ API设计在Web服务开发中是有一席之地的。此外,如果你使用C++来开发核心API,不仅仅是可以得到一个高性能的Web服务,而且你的代码可以通过其他方式来发布你的产品以达到重用的目的,例如发布成桌面或者手机版本。
顺便提一下,软件开发策略的变化是全球化作用的结果 (Friedman, 2008; Wolf, 2004)。实际上,因特网的同质化、标准的网络协议和Web技术创造了软件应用领域的新标杆。这让全球的公司和个体都可以给大型复杂的软件项目做出贡献。这种形式的全球化让无论在世界的哪个角落的公司和个体都可以开发软件的子系统,并可以此为生。在世界其他地方的组织可以通过组合和加强这些构件模块来解决特定的问题。从这里讨论的关注点上来看,API为现代软件设计的全球化和组件化提供了实现机制。
1.3.3 并行开发
即使你正在编写供内部使用的软件,你的同事写代码时也很可能要利用你的代码。如果你基于优秀的技术设计API,可以让他们开发起来更轻松,你也轻松(因为你不需要花太多时间回答你的代码是如何运作或者如何使用)。这点变得更加重要,如果多个开发人员同事开工且在代码上互相依赖。
例如,假设你正在开发一个字符串加密的算法,另一个开发人员要用来把数据写到配置文件中去。一种方法是,其他开发人员要等你开发完毕后再利用到写文件模块中去。然和,更有效率的方法是,你们两个事先见个面,商定一个适当的API。接着你就可以让API来代替那个功能占位符,而你的同事可以立即开始调用了,例如:
[代码P9 第一个]
你可以给这些函数功能提供一个简单的实现,至少让模块可以通过编译和链接。例如,.cpp文件可以如下所示:
[代码P9 第二个]
这样,你的同事就可以使用这个API,并可以继续他们自己的工作而不会被耽搁进度。从目前来看,你写的API实际上尚未加密任何字符串,但是这只是一个很小的执行细节!最重要的一点是,你写好了一个稳定的接口,一个协议,根据你们双方的共识达成的,而且它的功能是合适的,例如,Decrypt(Encrypt("Hello"))
== "Hello" 。当你完成这个API开发并更新了正确的实现时,你同事的代码就可以全部顺利运行,而且并不需要在他现有代码上做更多的修改。
在现实中,很可能遇到在你开始编写代码时没有预料到的接口问题,你可能需要多次迭代API来让它运行正常。然而,在大多数情况下,你们可以并行工作,而干扰却减到最小。
这种方法也支持测试驱动(test-driven)开发和测试优先(test-first)开发。通过早期中断API,你可以通过单元测试来验证预期的功能,通过不间断地运行确保不会违反你和同事制定的协议。
把这个过程放大到组织级别,你的项目可以有不同的团队,他们相隔甚远,甚至有不同的工作时间表。通过预先定义每个团队的依赖关系和API来创建这些模型,每队可独立工作,通过API,只需要知道最少的内容就可以让其他团队完成他们的工作。这种高效地资源利用,并相应减少通信冗余,可以为整个组织节约相关的开发成本。
1.4 何时应避开API?
比起编写普通的程序代码,设计和实现API通常需要更多的工作量。这是由API的目的决定的,它要提供给其他开发人员使用,就需要更加健壮和稳定的接口。因此,API的质量等级、计划、文档、测试、支持和维护的要求都比只在单个程序中使用的软件要高很多。
因此,如果你正在编写一个内部模块,不需要和其他用户进行通信,你不值得为模块创建一个稳定的公共接口开销,尽管这不是认真编写代码的理由。从长远来看,花上额外的时间来坚持API设计原则的努力是不会白费的。
考虑到另一方面,假设你是开发人员,需要在你的程序中使用一个第三方API。上一节讨论了为什么要在你的软件中重用外部API的很多原因。然而,可能在某些情况下你希望避免使用特定的API并靠自己实现代码或者寻找一个替代的解决方案。例如:
q许可限制 一个API可能提供你所需要的所有功能,但是许可证限制可能让你望而却步。例如,你如果你想使用一个开源包,根据GNU通用公共许可证(GPL),你所发布的任何派生作品都要符合GPL。这意味着,在你的程序中使用这个软件包,你就要公开你程序的整个源代码,商用程序很难接受这个约束。其他许可证,例如GNU次通用公共许可证(LGPL),就没有那么严格,在软件库中也更常见。另一种许可是要付费的,这种商用API可能对你的项目来说要价太高了,或者是许可条款限制太多了,比如每个开发人员都要许可费,甚至是每个用户。
q功能不匹配 API或许可以解决你的某个问题,不过也可能在某种程度上不满足程序的功能需求或约束。例如,你正在开发一个图像处理工具,你需要傅里叶变换的功能,也有很多快速傅里叶变换(FFT)可供选择,但是这些大部分是1D(一维)算法,而你需要的是2D FFT,因为你要处理的是2D图像数据。此外,很多2D FFT算法只能处理维度是2的幂次方的数据(例如,256 x
256或者512x512像素)。此外,你找到的API可能并不适用于你要运行的平台或者性能上无法满足你程序中的要求。
q
q源代码缺失 虽然有很多开源的API,你可能遇到最好用的API是封闭源代码的。也就是说,只有接口的头文件是可用的,而底层的源文件并没有和库文件一起发布。这会导致几个重大的影响。其中,特别是当你遇到库中有错误的时候,你无法通过检查代码来了解什么出了问题。检查源码,追踪错误和发现潜在问题是一项很有用的技术。
此外,无法访问API的源码,你就无法修复源码中存在的错误。这意味着,你的软件项目进度可能会受到你所使用的第三方API(当有无法预料到的问题时)的影响,你只能等待这个API的发行方来解决你提交的错误报告和发布一个补丁包。
q文档缺乏 API可能可以满足你程序中的所有需要,但是如果由API提供的文档质量很差或者根本没有文档,你就可能要寻找其它的解决方案。或者是不知道如何使用API,或者是不确定在什么情况下使用,或者是你根本不相信开发这个API的工程师,因为他连如何使用他的代码都懒得写。
q
1.5 API例子
API随处可见,即使你编程的经历不长。你可能在编写代码时利用过一两个API,或者是靠自己写一个。
1.5.1 API的层
API可以是任意大小,从单一功能到很多类的大集合。它可以提供访问任何架构层的功能,从底层的操作系统的所有调用到GUI功能。下面的列表给出了各种常见的API,可能有很多你已经听说过了,现在看看这些流行的API:
q操作系统(OS) API 所有的操作系统都必须提供一组标准的API来允许程序访问系统级的服务。例如,POSIX(译者注:Portable Operating System Interface based on UNIX 基于UNIX的可便携操作系统接口) API定义了处理UNIX进程的函数,如fork()、getpid()和 kill()。微软的Win32
API 包含CreateProcess()、GetCurrentProcess()和
TerminateProcess () 函数,用来处理Windows进程。这些都是稳定可靠的底层API,不会更改,否则会导致很多程序崩溃!
q编程语言API C语言有提供一个标准的API,由libc库实现并包含操作说明,它包括我们熟悉的函数,例如printf()、scanf()和fopen()等。C++语言也有提供标准模板库(STL),它包括了各种容器类的API(如
std::string、std::vector、std::set 和 std::map等),迭代器(如std::vector<double>::iterator等)和泛型算法(如std::sort、std::for_each、和std::set_union)。下面的代码片段利用STL API遍历一个向量并输出:
[代码 P12 第一段]
q图像API 现在已经不需要由开发人员自己编写读写图像的代码了,有很多开源包可供你下载并可以在你的程序中使用。例如,有一个很流行的libjpeg库提供对JPEG/JFIF的解码和编码的功能。还有libtiff库用来读写各种TIFF文件。而libpng库用来处理PNG格式的图像。所有这些库所定义的API可以帮助你编写代码以读写各种图像格式,而且你不需要知道任何图像格式底层的细节。例如,下面的代码片段使用libtiffAPI来获取图像的尺寸:
[代码 P12 第二段]
q三维图形API 最优秀的两大实时3D图形API是OpenGL 和 DirectX。这些可以让你通过基本的几何结构来定义3D物体,例如三角形和多边形;还可以指定这些几何结构的表面属性,如颜色、法线和材质;还能定义环境状态,如灯光、雾气、剪贴区(clipping
panes)。多亏了这些标准的API,游戏开发人员能够编写在新老显卡(来自不同厂家)上运行的3D游戏。那是因为每个显卡制造商发布的驱动都有提供基于OpenGL 和 DirectX API的实现细节。在这些API广泛使用之前,开发人员只能给某个特定的图像硬件编写3D程序,而这个程序很可能无法在其它不同的图像硬件设备上运行。这些API也可以作为其它更高级别场景图形API的载体,例如OpenSceneGraph、OpenSG、和
OGRE。下面的代码片段演示一个绘制三角形的典型例子,每个顶点都采用不同的颜色,使用的是OpenGL API:
[代码 P13 第一段]
q图形用户界面API 任何应用程序,只要打开其窗口都需要用到GUI工具包。这个API用来创建窗口、按钮、文本框、对话框、图标和菜单等,还提供了事件模型用来捕获鼠标和键盘事件。一些流行的C/C++图形用户界面API包括wxWidgets库、诺基亚的Qt API、GTK+和X/Motif。以前的情况是这样的,如果一个公司在多个平台上发布程序,比如Windows和Mac上,那么他们将不得不为每个平台使用不同的GUI
API来编写用户界面代码,或者他们必须开发可供自己使用的内部跨平台GUI工具包。然而,现在大部分GUI工具包可供多个平台使用,包括Windows、Mac和Linux,编写跨平台程序变得简单得多了。举个跨平台GUI API的例子,下面的程序显示了一个小而全的Qt程序,当点击Hello World按钮时,弹出一个窗口。
[代码 P13 第二段]
当然,这个例子只是很多跨平台API的一个简单例子。你还可以找到通过网络访问数据的API、解析和生成XML文件的API、帮助你编写多线程程序的API和解决复杂数学问题的API。上面提到的这些只是要说明API应用的广度和深度,API对你开发程序很有帮助,且让你了解下什么代码可以利用这些API。
提示
在现代软件开发中,API随处可见,从系统到语言级别的API,再到图像、音频、图像、并发、网络、XML、数学问题、web浏览或GUI API。
1.5.2 真实案例
上述的API例子是根据架构层(architectural level)来编排的,展示了你在构建一个程序时可能会用到的API范围。当你在构建一个大型软件产品时,你将常常用到几个架构层上的API。例如,图1.3给出了由Linden实验室开发的Second Life Viewer架构图的一个例子。这是一个大型的开源程序,允许用户在一个在线3D虚拟世界中相互交流,是通过语音聊天和文本消息来实现。该图展示了API在大型C++项目中的用途和分层。
特别值得注意的是内部API层,模块集是某个公司针对特定产品或组合开发的内部产品。而图1.3为了简化的目的只把这些显示为一个单一的层,内部的API集将形成一个额外的堆叠层。从一些基础的例程提供的字符串(string)、字典(dictionary)、文件IO(file IO)和线程例程(threading routine)等到API提供的核心程序业务逻辑,所有的自定义GUI API都是用来管理程序的用户界面。
显然,图1.3并没有列出应用程序中所有使用的API清单。它只显示了每个架构层的几个例子。然而,表1.1给出了程序中用到的第三方依赖的完整集合,让你了解可以有许多开源和商业闭源同时依赖在一个软件项目之上。当你深入到系统级和操作系统库时,这个清单的内容将增长不少。
[图 P14 1.3]
图1.3
Second Life Viewer架构图
表格1.1 Second Life Viewer中使用的开源和闭源API
[表格 P15 1.1]
API和SDK
术语软件开发工具包(Software Development Kit SDK)和术语API紧密相关。从本质上讲,一个SDK是一个安装在你计算机上的特定平台的包,依靠一个或多个API来构建程序。
一个SDK至少要包含头文件来编译你的程序和库(.dylib、so、.dll)文件来链接到你的程序以提供API的实现。然而,SDK可能还包含其他资源,帮助你使用这个API,例如,文档、示例源代码和支持的工具。
举个例子,苹果发布的各种iPhone API让你可以编写运行在iPhone、iPod Touch和iPad设备上的程序。这些例子包括UIKit用户界面API、把Web 浏览器功能嵌入到程序中的WebKit API和用来音频服务的Core Audio API。
苹果还提供了iPhone SDK,它是一个可下载的安装程序,包含实现各种iphone API的框架(头文件和库)。这些文件,通过编译和链接来让程序可以获得API的基本功能。iPhone SDK也包含了API文档、示例代码、苹果的集成开发环境(Apple’s Integrated Development Environment)提供的各种模板,它叫做XCode,还有iPhone 模拟器,可以让你在电脑上运行iPhone程序。
1.6文件格式和网络协议
计算机程序中还有几种其它形式的通信“协议”。其中最熟悉的就是文件格式,就是通过一种众所周知的布局把内存数据保存到磁盘中。例如,JPEG文件交换格式(JFIF)是一个交换JPEG格式图像编码的图像文件格式,通常使用.jpg或jpeg作为文件扩展名。JFIF文件头的格式如表1.2所示:
对于一个给定的数据文件格式,例如表1.2中的JFIF/JPEG格式,任何程序都可以读写该格式的图像文件。这允许在不同用户之间交换数据,图像浏览器快速加载和通过工具操作那些图像。
同样,客户端/服务器端(client/server)程序,点对点(peer-to-peer)程序和利用现有的协议回送与转发数据的中间件(middleware)服务,通常都是通过网络socket来实现。例如,Subversion版本控制系统就是采用客户端/服务器端架构,主资料库是存储在服务器上,而个人客户端是和服务器来进行同步的(Rooney, 2005)。为了让这个能够运行,客户端和服务器端必须商定好这些数据通过网络传输的格式。这个被称为客户端/服务器端协议或线路协议。如果客户端发送的数据流不符合这个协议,那么服务器端将无法理解这个消息。因此,至关重要的是,客户端/服务器端协议的规范和界定,同时客户端和服务器端都必须遵循这个规范。
这些情况都在概念上类似API,因为它们也都有为信息交换定义标准的接口和规范。此外,任何规范的改变也都要考虑到对现有客户的影响。尽管有这种相似性,文件格式和线路协议并不是真正的API,因为它们并不是编程接口,让代码可以链接到程序中去。然而,一个好的经验法则是,只要你有一个文件格式或客户端/服务器端协议,你也应该有一个相对应的API来管理这个规范的变化。
[表格 1.2 P16]
提示
只要你创建一个文件格式或者客户端/服务器端协议,你也要为它创建一个API。这允许规范细节在未来任何时候发生变更时,都可以被隐藏和集中化。
例如,如果你指定一个应用程序的数据文件格式,那么你应该写个API用来读写这个格式的文件。其中之一是,这仅仅是良好的做法,文件格式消息不会分布到整个应用程序中去。更重要的是,使用API允许你轻松地更改,而将来无需在API实现之外重写任何代码。最后,如果你最终得到的文件格式有多个不同的版本,那么利用API可以把复杂性进行抽象,这样就可以读写格式的任何版本,或者也可以知道是否该编写一个API的新版本,并进一步采取适当的步骤。从本质上讲,磁盘上数据的实际格式隐藏了实现细节,你的应用程序并不需要与之直接联系。
这个建议同样适用于客户端/服务器端程序,它有共同的协议定义、共同的API来管理该协议,可以让客户端和服务器端相对独立的工作。例如,开始你可能使用UDP作为传输层并成为系统的一部分,但后来决定切换到TCP(这个在Second Life代码库中发生过)。如果所有的网络访问已经通过一个合适的API抽象过了,那么这个这么大的实现变更对系统的其他部分并不会造成什么影响。
1.7关于本书
现在我已经讲述了API是什么的基础知识和API开发的利弊。接着,我会深入探讨如何设计良好API的细节,如何在C++中高效的实现它们和如何进行版本控制而不至于破坏兼容性。本书的章节大致如下,一步步深入,从最初的设计到实现、到版本控制、到文档和最后到测试。
第二章:品质
我用一章来回答下列问题的主要内容:什么是一个良好的API?其中涉及到的特质是在你设计API的时候应该知道的,如信息隐藏、最小完整性和松耦合(loose coupling)。正如我在全书中所做的,我通过很多C++源代码的例子来演示这些概念,让你知道它们如何应用到你自己的项目中去。
第三章:模式
在接下来几个章节中解决如何设计一个良好API的问题。因此,第三章着眼于一些具体的设计模式和在API设计中很有帮助的习惯用法。这些包括pimpl idiom、Singleton、Factory Method、Proxy、Adapter、Fac、ade、和 Observer。
第四章:设计
在讲述了如何设计一个良好的API这个主题后,第四章讨论的是收集功能需求和用例建模(use case modeling),可以设计出简洁和实用的接口。此外,还讨论了一些面向对象设计和分析的技术。本章还包括对一个大型软件项目的许多问题的浅显讨论。这些见解都是来源于现实工作中的经验,当在大型API设计出现问题时,可以对问题有较深的理解。
第五章:代码风格(Styles)
接下来的几个章节将专注于利用C++创建高质量的API。这是本书的重点,也是比较深奥和复杂的主题。开始我先描述各种C和C++ API的代码风格,你可以在项目中采用,例如纯C的API、面向对象的API、基于模板API和数据驱动(data-driven)API.。
第六章:C++ 用法
接下来我将讨论可能会影响设计良好API的各种C++语言特性。这包括良好的构造和编码风格、名空间、指针和引用参数、协同开发以及如何在一个动态库中导出符号(symbol)。
第七章:性能
本章我分析了API中的性能问题,并为你讲述如何利用C++来构建高性能的API。这些包括常量引用(const references)、前向声明(forward declarations)、数据成员簇(data member clustering)和内联(inlining)。我还会介绍几种工具来帮助你对代码进行性能评估。
第八章:版本控制
在掌握了API设计的基础之后,我开始扩展到更深方面的内容,先从API的版本控制和如何维持向后兼容性讲起。对设计一个稳定耐用的API来说,这个部分是最重要,也是最难的。这里我会介绍各种术语:向后(backward)、向前(forward)、功能的(functional)、原始码(source)和二进制兼容性(binary compatibility),还有如何优化一个API来最大程度减小对客户的影响。
第九章:文档说明(documentation)
接下来,本章讲述的主题是API的文档说明。一个API缺少必要的支持文档,这就会导致出现描述不清的情况。我这里将讲述为API提供注释和文档化的好技术,给出的例子将使用一个非常棒的工具Doxygen。
第十章:测试
使用深度测试来优化你的API可以让你自信满满地确定不会破坏客户端程序。这里我将给出几种测试,自动化测试(automated testing)、单元(unit)、集成(integration)和性能测试,这些符合测试方法论的例子都可以很好地应用到你的项目中去。这还涵盖了诸如测试驱动开发、存根和模拟(stub
and mock)对象、测试私有代码(testing private code)和契约式编程(contract programming)。
第十一章:脚本
接下来几个是专门的主题,从API脚本开始。这是一个可选的主题,并不适用于所有API。不过,你可能决定提供通过脚本访问API,使你的应用程序用户可以编写脚本来执行自定义操作。因此,我将讲述如何创建脚本以绑定到C++ API上,这样就可以通过Python和Ruby语言来调用。
第十二章:可扩展性
另一个高级主题是关于用户扩展性的:创建的API允许程序员编写自定义的C++插件,该插件扩展了基于API上的基础功能。这个机制是很重要的,对API的扩展,让它的生存期更长。此外,通过继承和模板,我将介绍如何创建可扩展的接口。
附录A:库
本书的最后篇幅是关于如何创建静态和动态库的附录。你必须能够创建代码库以供其他用户使用。还有界面设计时需要考虑的问题,如你导出的公开符号集。我还讨论了静态库和共享库的区别,并演示如何让你的编译器生成可供其它应用程序重用的库。