中国开发网: 论坛: 程序员情感CBD: 贴子 690821
haitao
好的API:API设计的“不可承受之轻”
http://www.infoq.com/cn/news/2007/09/why-api-design-matters

API设计的“不可承受之轻”
作者 Niclas Nilsson译者 李剑 发布于 2007年9月3日 下午7时46分

社区 Architecture 主题 编程, 设计 标签 文档, 生产力, 检查清单和指南
API的设计影响着所有的开发者。有些API用起来很舒服,而有些则用起来让人焦头烂额,更有甚者,让人完全丧失了继续用这套API来做开发的勇气。但它们的区别在哪里呢?是哪种品质会让一套API易用而另一套复杂难解?ACM Queue最近刚发布了Michi Henning的一篇有关API设计的文章,作者在文中剖析了API的好坏之分,并指出了API对生产力的影响。

当我们开始用API的时候,我们就能知道它是好是坏。好的API会让人心情愉悦,用起来得心应手:当你想调用某个特定的任务时,最合适的方法会在最恰当的时候出现在你面前,容易发现,易于记忆,文档说明详尽,接口设计符合人的思维习惯,而且正确的处理了边界条件。

那为什么现在还有这么多垃圾API呢?其主要原因在于,对于每一种正确的API设计方式而言,都同时伴随有很多种错误的设计方式。简单来说,设计一种坏的API要比设计好的API容易的多。即使是很微不足道的、无伤大雅的设计上的小小缺陷,最终也会被放大成巨大的阴影,因为API只需要提供一次,但是却会被调用很多次。如果一个设计缺陷会导致笨拙或是低效的代码,那么这个问题就会在API每次被调用的时候都显现出来。而且,有些设计缺陷单独看起来很小,但是却会以令人讶异的破坏性的方式来相互影响,从而很快就会导致不计其数的间接性破坏。

Michi在给出自己对API设计的建议之前,先举了一个反例为证。他对.NET框架中的Select()方法调用进行了分析,展示了很多API中都存在的共通问题。


Select() 方法中传入的参数是需要进行监控的socket列表。在大多数应用程序中,所使用的socket不会常常变化,所以这些列表基本上在很长一段时间内都是不变的。但是:

因为Select()方法对参数进行了重写,所以调用者在把每一个列表传入Select()方法之前必须要保留一个备份。这就给用户带来了很大的不便,而且不宜扩展应用的规模:服务器常常都要监控上百个socket,而在每一次循环中,调用Select()方法前都要复制所有的列表。
因为socket会进入等待或是阻塞状态,所以Select()还接受一个时间参数用来标识超时,如果在参数所指定的时间内没有任何socket可用,那么方法就会返回。但是,因为这方法是void类型,所以没有很便捷的方式可以标明Select()方法是因为有了可用的socket而返回,还是因为超时返回的。

为了判断是否有可用的socket,调用者必须要对方法参数中的三个列表各自的长度进行测试;如果三个列表长度都是零,那么就是没有可用的socket。如果调用者碰巧很关心这一点的话,那就要写一个相当别扭的测试了。更有甚者,如果没有socket就绪而发生超时,那么该方法就会销毁调用者传入的参数,所以就算是什么事情都没有发生,调用者还是必须要在每一个循环中对这三个列表进行备份!
而且,在API文档中,也没有明确的指出该怎样无限期地等待socket就绪。Michi还是通过自己的试验才发现,如果超时参数为0或是负数,那么该方法就会立刻返回,而且也没有任何办法可以让该方法等待的时间超过35分钟。这就逼着他只好自己实现了一个Wrapper,来完成持续性等待的功能。

在写这个Wrapper的时候,Michi又发现了API设计中新的潜在问题:

Select()方法的另一个问题就是它接受的参数是socket列表。列表中可以允许同一个socket多次出现,但是这样做一点意义都没有:从概念上讲,传入的应该是由socket组成的set。那么为什么Select()会使用列表(List)呢?原因很简单:.NET容器类中没有包括对set的抽象。使用IList来对set建模是让人徒呼奈何的一件事情:它会带来语义上的冲突,因为list允许出现重复的元素。(Select()方法遇到重复的socket会发生什么样的行为大家只能瞎猜,因为没有明确的文档记录;而要实际去检测它的真实行为也是没有意义的,没有文档说明,我们怎么知道实现会不会在什么时候突然改变?)
一个劣质的API设计所带来的问题就是,差不多每个用这套API的开发人员都要设法去弥补其中的缺陷。该API的用户群越大,开发人员浪费的时间也就越多。

劣质的API不但会带来更多的编码,而且代码结构也会更加复杂,潜藏bug的地方也就越多。
不过设计错误所发生的位置不同,其后果也会有所差异。

在抽象层次中,如果API缺陷出现的位置越低,其后果也就越严重。如果我在自己的代码中错误地设计了一个函数,那么影响到的人就只有我自己而已,因为我是这个函数的唯一调用者。如果我在我们的项目库中错误地设计了一个函数,那么或许所有的同事都会遭罪。如果我在一个广泛采用的代码库中错误的设计了一个函数,那么或许就会有成千上万的程序员开始诅咒了。
任何大面积流行的公共代码库或多或少都会被认为是不可变的。这种类型的API的任何变化都有可能破坏向后的兼容性,引起大量问题。方法声明的变化会导致客户端无法编译或是崩溃,方法行为的变化会导致无可预计的微妙错误。即使是修复bug也会有问题,因为会有些既有的代码是依赖于原始的有bug的行为的。


在动态链接库的世界中,客户端代码就更容易受到API变化的影响了。哪怕采用了版本机制,客户端代码又怎样判断库文件版本号的小小变化是否会影响到当前任何对API行为的假定呢?


即使API自身的变化会让框架变得更好,但是每一个用户也都要为升级付出一定的代价。当然,也有的变化不会引起客户端哪怕一行代码的改动,可就算这样,每一个客户端也不得不进行重新编译,以防止出现崩溃的情况,因为源码不变的情况下,二进制码也会发生变化。

一个API设计者需要考虑很多事情,当然,大多数的决定都是根据过往经验做出的,不过还是有些比较优秀的想法应当推荐给大家。Joshua Bloch在Javapolis/InfoQ上关于API的演讲中和听众分享了自己的心得,而在他所有的建议中,最根本的一条就是要让API尽可能地小(但绝不过分小),因为:

你可以随时往里面加东西,但是你永远不能删除些什么。
Joshua讨论了几乎每一件应当注意的事项,从不可变性(保证类的不可变,除非有比较好的理由要求可变)等概念的重要性,到比较细微但也同样重要的细节问题,比如只要在可能的情况下,就宁可返回空的列表也不要返回null值(因为返回一个null值会导致调用者要做多余的检查)。

Michi Henning同样在文中也给出了他的一些如何让API更加优秀的建议:

API必须要提供充分的功能,以供调用者完成自己的任务。
API应该是最精简的,不要为调用者带来多余的不便。
如果没有理解API的使用环境的话,那也就不能去设计它。
通用性的API应当是与具体使用场景无关的,而特定用途的API则要充分考虑使用策略。
API应该从调用者的角度来进行设计。
好的API绝不推卸责任,把自己该做的事情留给别人。
在实现API之前,就应该把API文档化。
好的API应当符合工效学(Ergonomic)。

因为大多数人都会承认这些建议的重要性,人们会觉得应该在学校里教授这些知识,但是看起来这还不属于当前的标准教学过程。Ed描述了现在的课程中这些知识的欠缺,作为对Michi的文章的评论:

我现在意识到了我所接受的教育是不完整的。我上的课只是教会了我们怎样寻找最好的算法,或是和旧的数据结构行为相似的新数据结构(还有人记得双向队列么?)。而且我们的作业也从来没有根据设计来评分:评分的依据纯粹是执行速度。
但是如果教育系统不教给学生如何进行API设计的话,那么资深的同事会教么?Michi在这一点上不甚乐观:

经验的积累是需要时间的,而且还要经历过一点“吃一堑,长一智”,才能掌握如何把事情做得更好。但不幸的是,我们这个行业的趋势就是把经验最丰富的开发人员提升到远离编码的职位上,正好是在他们可以把多年积累的经验用到实处的时候。
这就让整个的学习过程变得很棘手了。那么,我们这整个行业该做出哪些改进呢?
我的blog:http://szhaitao.blog.hexun.com & http://www.hoolee.com/user/haitao
--以上均为泛泛之谈--
不尽牛人滚滚来,无边硬伤纷纷现 人在江湖(出来的),哪能不挨刀(总归是要的)
网络对话,歧义纷生;你以为明白了对方的话,其实呢?

您所在的IP暂时不能使用低版本的QQ,请到:http://im.qq.com/下载安装最新版的QQ,感谢您对QQ的支持和使用

相关信息:


欢迎光临本社区,您还没有登录,不能发贴子。请在 这里登录