http://blog.csdn.net/horkychen/article/details/46612899

API的设计是软件开发中一个独特的领域。最主要的特殊点在于API是供开发者使用的界面,即Application Programmer Interfaces。类似于用户可以直接使用到的GUI的作用一样。所以相对于依据软件设计的原则,考虑用户的”体验”会更加重要。

许多著名的工具和库的作者都写过相关的著作,详细的论述他们在API上的设计与实现要点。下面的论述,就是从这些前人的工作成果中总结而来。以下先列出参考资料:

关于API

狭义上API可能只是一个动态库(共享库)提供功能的接口定义。广义上API分为public API,以及internal API之分。既有整体软件系统对外输出的接口(包括与设备通讯的接口),也有系统内一个底层模块提供给上层模块使用的接口定义。

API看似简单的名词,却代表着重要的架构设计。从架构设计的角度来看(所谓的组成论),软件系统就是模块和接口。模块(层次/组件)决定分工,接口决定交互。API就是接口的定义。模块间并不需要关心其它模块的实现,只需要了解如何进行协作即可。这样将复杂度分散到各个模块之中,使得整体系统更为可控。而API的本质,就是提供给模块开发者使用的接口,是给”人(Programmer)”用的。API的设计任务的核心就是保证使用者以较低的成本,正确的使用接口,驱动模块完成他们的业务。对于Public API,最大的设计挑战则是如何把API一次就做对!

附1的作者在书中提到了一个”无绪(cluelessness)”的概念,即API的使用者不需要对API的内在逻辑有了解,可以只依据API的定义来使用API。更直白一点就是傻瓜式的API。


什么是好的API

对于一般的开发任务,常常思考的是保证功能的正确性和设计的完美,可以不断尝试做创新和重构。但这些原则放到API设计上就不一定正确了,反而需要有些保守。先看一下KDE/Qt开发者总结出来的好API标准:

容易学习和记忆

(Easy to learn and memorize) 
这包括了命名,模式的使用,最关键是对于经验式编程的包容。所谓经验式编程是指开发者常常不会认真读完接口的文档(如果提供的话),而是根据思维的连续性,以过往的经验来预先假定API的功能。比如,如果如下两个类都有相同方法:

void Widget::SetSize(int width, int height);
void View::SetSize(int width, int height);

另一个类,逻辑上会自然的认为是View的子类,但却提供如下的方法,就会让人捉摸不透了:

void Button::Layout(int width, int height);

从经验式编程的角度,使用Button::SetSize()是非常自然的事,程序员很可能不会认真核实这个Button竟然没有提供这个方法。 
作为API设计者,不能假定使用者都会认真的看完所有的文档,而是要尽量做到两点:

  • 保持与普遍认知一致的设计。
  • 保持设计概念上的一致性(Consistency)。

那些被公认的行为和命名就非常重要,千万不要做太多创新。请遵守最小惊喜原则。

简洁清晰的语义

这样有助于理解,也很难被误用。当一个API无法满足所有的需求时,不要尝试为了一些极小场景来影响到一般的场景,可以另分一个独立的路径。这样的情况,往往反应在函数的参数上。比如这样的API(来自Win32), 你必须每次都要对着文档来调用了:

HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam);

另外在附2里举了一个输出如下HTML文本的例子:

the <b>goto <u>label</b></u> statement

以C++的实现可以为:

stream.writeCharacters("the ");
stream.writeStartElement("b");
stream.writeCharacters("goto ");
stream.writeStartElement("i");
stream.writeCharacters("label");
stream.writeEndElement("i");
stream.writeEndElement("b");
stream.writeCharacters(" statement");

很显然,这里Element的Start与End需要开发者自己处理。如果想要编译器来帮助检查,让开发者少犯错,则代码可以变为:

stream.write(Text("the ")
+ Element("b", Text("goto ") + Element("u", "label"))
+ Text(" statement"));

容易扩展及保证向后兼容

之前的资料都是分散的谈到两者的,我将它们合并在这里,因为它们都是API演变所必须考虑的。 
随着需求变化,API的演变是必须的,不可能存在一成不变的API。但是作为稳定的API则是对使用者的承诺,不单单是技术上。稳定的概念不是不变,而是指变化的成本要尽可能的低。 
如果新增一个API会导致之前的代码无法编译,或者程序无法正常执行,都会影响使用者对API的信任。

能够鼓励编写可读性代码

还是前面强调的,API是给程序员用的,所以本身的命名必须具备可读性。同时,它还要设计成引导使用者写出更具可读性的代码。附2里举了如下的例子。  
在Qt3中,Slider的建构函数允许用户指定多个参数:

slider = new QSlider(8, 128, 1, 6, Qt::Vertical, 0, "volume");

而在Qt4,则需要这样做:

slider = new QSlider(Qt::Vertical);
slider->setRange(8, 128);
slider->setValue(6);
slider->setObjectName("volume");

显然后者更具可读性。

这里还是有争议的。既不能为单独的追求可读性而将相关的东西分离开来,也不能为了简化代码,而将不同的内容合在一起。

简洁

这一点对于第一条特别重要。一个不断膨胀,十分臃肿的API必然会产生各种理解和使用上困扰,特别是当多个API存在功能重叠的情况时。举一个会带来理解上困扰的例子: 
void View::SetSize(int width, int height); 
void View::SetWidth(int width); 
void View::SetHeight(int height); 
后两者明显是前者的两个子任务,却因为某些特别的原因被公开出来。就会出来到底是调用SetSize(),还是根据变化调用对应的SetWidth()或SetHeight()呢?

完整

如果需要提供的功能就要提供,一个接口类应当具备的函数(包括setters/getters)也应当在这个类中提供。


API的设计实现

关于API的设计实现,不同的背景,不同的需求会有不同的描述了。我这里概括了一些他们间相通的要点。

工厂方法优于建构函数

如果公开一个构造函数,那么创建的对象一定是类的实例。而工厂方法更具灵活性,虽然参数完全相同,但可以返回一个子类的实例。同时更利于实现单例或者缓存对象实例。 
在Chromium一些模块的接口上,常常可以看到这类的应用。

常量修饰符

常量修饰符,有助于限定不必要的修改动作,也是一种行为约定。无论是对参数,函数,或是返回值,都可以视需要添加常量修饰符。

基于属性的API

相对于在建构时传入一串参数的接口类,不如在建构后再以setter设置其它参数的方式。其区别在于后者更利于编写可读性的代码。在上面关于可读性代码中已举过例子,这里不再赘述。 
要点是各个属性需要做到正交,且与顺序无关。

Virtual APIs

对于是否需要提供虚函数形式的API,也是一直有争论。这里并不是讨论接口类(纯虚类)的定义,接口类的定义的必要性是明确的,不需要额外讨论。 
原则上对虚函数作为API是限制使用的,原因是继承下的override可能会导致接口的行为变得不符预期,因为子类的行为无法确定。 
但在一些场景下确实有必要为使用者提供一定的扩展性,就可以提供虚函数,以便使用者可以通过继承改变原来的行为。

布尔值参数

以整型数据代替Enum的作法类似,关键在于使用者的理解。 
可以改进的做法包括,分成不同的函数实现,或者以枚举变量代替。 
示例:

widget->repaint();
widget->repaint(true);
widget->repaint(false);

分开函数的方式:

widget->repaint();
widget->repaintWithoutErasing();

使用整数代替格枚举变量时也是相同的问题。

异常处理

在附5中作者详细说明了关于API中的异常处理。我的总结是只抛必须抛的异常,绝不能自作聪明的默默处理。API的代码应当最真实的反应出执行中的问题,更不能用聪明的代码做某些特别处理。其背后的原因是这样做会使得API的行为与预期会发生偏差,违背了最小惊喜原则。

命名

在命名上,附2列举的比较详细。概括如下:

  • 选择具有自解释能力的命名 
    核心是从用户和领域的角度命名,而不是从自身的设计命名。比如Qt 4.2中QWorkspace实现了MDI (multiple document interface)。好在这样的命名后来被修正为QMdiArea。
  • 命名不要有歧义 
    如果遇到有概念相似的API,一定要从命名上将它们区分出来。如sendEvent()表示同步的事件,而sendEventLater()则表示异步事件。
  • 保持一致性 
    这一点对于前面对经验式编程的支持很重要,也被称为对称性(Symmetry)。如果set前缀代表的是setters,就不要出现以set打头,但却不是setter的情况。再比如Chromium中对setters/getters的定义以非常明确的方式独立出来。
  • 避免简写 
    简写除了是某种通用的缩写外,不要随意以首字母缩写的形式定义简写。不然,读者可能对名字完全不知所云。
  • 优先使用特殊的命名,而不是通用的命名 
    一个通用的名字常常包含更为普遍的职责,如果API的功能带有明确的应用场景,就应当在API上体现出来。否则一旦遇到需要一个通用API的情况,就用很多余的加上XXXXInGeneral之类的命名,而且会让用户出现难以选择适用API的情况。
  • 不要太迁就于既有的命名 
    比如包装一个旧的或子功能的API的时候,常常会延用原有的API命名。其实完全没必要,更合理的做法还是从新API的功能入手,选择合适的名字。

关于向后兼容

一个模块(库)的兼容性主要包括:

  • API兼容 
    主要是定义上的兼容性,即代码能否编译,以及行为的一致性。

  • ABI兼容,即二进制级的兼容。 
    对于共享库就是需要有相同的符号表,包括全局的对象和定义。Linux里这类问题太多了。

  • 通讯协议的兼容 
    如果有自定义协议的网络通讯,就可能存在C/S之间通讯协议的兼容性问题。

  • 存储的数据及文件格式的兼容 
    如果用户升级后,发现以前的历史数据不可用了,大多数情况都是无法接受的,搞不好还要吃官司的。

保证兼容性

至于要保证哪些点的兼容性,取决于用户的规模,以及影响的程度(或者用户的承受能力)。从兼容性的角度,保证兼容性方法包括:

  • 不要丢掉任何东西 
    非常悲催的现实。如果你弃用了API的某一部分(更不能改了),无论使用@Deprecated,还是在文档中反复声明,你都可能会造成使用者之前的代码失效。一定要保证之前API的完整性,除非你的兼容性规则允许你放弃,就比如像MicroSoft一样宣称将不再支持某个版本。

  • 隐藏细节 
    可以使用Opaque Pointer (PIMPL)或者利用建构函数来帮助API隐藏内部的数据结构,而且让使用者只能通过提供的函数来操作数据。

  • 保证协议及数据格式的扩展性 
    可以使用标准化的XML以及标准化的协议来取代自定义的格式。如果条件不允许,也记得在协议及数据格式中定义出版本,以便于后期做兼容性处理。 
    预留字段也是一个常用的做法。我曾经不止一次的遇到,通过协议中的预留字段解决紧急问题的案例。

  • 实现上保证兼容性 
    在实现逻辑上,特别是判断处理也要注意兼容性处理,这是一个常常犯错的地方。以某个字段flagA的处理为例:

    if (headers.flagA != 1) { 
    doB(); 
    } else { 
    doA(); 
    }

显然将判断条件改为headers.flagA == 1会让实现更具兼容性。否则,降级时,就是灾难了。


极端的意见有害无益

(主要参考附1) 
关于API定义的评价中,漂亮或者优雅都是很主观的。我们应当设计易于使用,广为接受且富有成效的API(节自附1)。至于所定义的原则,完合取决于API自身的需求。比如因为性能的原因,一些API可能无法满足某些场景的需求,达不到完整性的要求。API的设计者不需要去满足所有人,重要的是API本身保持正向的演进。比如标准的优化流程就比较适合API的发展: 
1. Make it work 
2. Make it right 
3. Make everything work 
4. Make everything right 
5. ……

转载请注明出处: http://blog.csdn.net/horkychen

关于API的设计与实现的更多相关文章

  1. RESTful API URI 设计的一些总结

    非常赞的四篇文章: Resource Naming Best Practices for Designing a Pragmatic RESTful API 撰写合格的 REST API JSON 风 ...

  2. RESTful API URI 设计: 查询(Query)和标识(Identify)

    相关文章:RESTful API URI 设计的一些总结. 问题场景:删除一个资源(Resources),URI 该如何设计? 应用示例:删除名称为 iPhone 6 的产品. 是不是感觉很简单呢?根 ...

  3. RESTful API URI 设计: 判断资源是否存在?

    相关的一篇文章:RESTful API URI 设计的一些总结. 问题场景:判断一个资源(Resources)是否存在,URI 该如何设计? 应用示例:判断 id 为 1 用户下,名称为 window ...

  4. Atitit.会员卡(包括银行卡)api的设计

    Atitit.会员卡(包括银行卡)api的设计 1. 银行卡的本质是一种商业机构会员卡1 2. 会员卡号结构组成1 2.1. ●前六位是:发行者标识代码 Issuer Identification N ...

  5. Web API接口设计经验总结

    在Web API接口的开发过程中,我们可能会碰到各种各样的问题,我在前面两篇随笔<Web API应用架构在Winform混合框架中的应用(1)>.<Web API应用架构在Winfo ...

  6. 优秀的API接口设计原则及方法(转)

    一旦API发生变化,就可能对相关的调用者带来巨大的代价,用户需要排查所有调用的代码,需要调整所有与之相关的部分,这些工作对他们来说都是额外的.如果辛辛苦苦完成这些以后,还发现了相关的bug,那对用户的 ...

  7. atitit.基于http json api 接口设计 最佳实践 总结o7

    atitit.基于http  json  api 接口设计 最佳实践 总结o7 1. 需求:::服务器and android 端接口通讯 2 2. 接口开发的要点 2 2.1. 普通参数 meth,p ...

  8. paip.复制文件 文件操作 api的设计uapi java python php 最佳实践

    paip.复制文件 文件操作 api的设计uapi java python php 最佳实践 =====uapi   copy() =====java的无,要自己写... ====php   copy ...

  9. 好RESTful API的设计原则

    说在前面,这篇文章是无意中发现的,因为感觉写的很好,所以翻译了一下.由于英文水平有限,难免有出错的地方,请看官理解一下.翻译和校正文章花了我大约2周的业余时间,如有人愿意转载请注明出处,谢谢^_^ P ...

随机推荐

  1. Netty解决TCP粘包/拆包问题 - 按行分隔字符串解码器

    服务端 package org.zln.netty.five.timer; import io.netty.bootstrap.ServerBootstrap; import io.netty.cha ...

  2. malloc/free和new/delete的区别

    转自:http://blog.csdn.net/chance_wang/article/details/1609081 malloc与free是C++/C语言的标准库函数,new/delete是C++ ...

  3. 自定义progressBar的旋转圆圈

    在手工打造下拉刷新功能 自带的progressBar太丑了 做个也不费事,一个简单的圆形 旋转动画加type是sweep的gradient渐变 <rotate //旋转动画xmlns:andro ...

  4. SpringMVC数据验证

    SpringMVC数据验证——第七章 注解式控制器的数据验证.类型转换及格式化——跟着开涛学SpringMVC 资源来自:http://jinnianshilongnian.iteye.com/blo ...

  5. [CareerCup] 7.7 The Number with Only Prime Factors 只有质数因子的数字

    7.7 Design an algorithm to find the kth number such that the only prime factors are 3,5, and 7. 这道题跟 ...

  6. Bootstrap 表格

    Bootstrap 提供了一个清晰的创建表格的布局.下表列出了 Bootstrap 支持的一些表格元素: 标签 描述 <table> 为表格添加基础样式. <thead> 表格 ...

  7. HDU5802-windows 10-dfs+贪心

    音量减的时候,分两种,一种是减到大于目标M,另一种是减到小于M,停顿的时候可以减少最后往上加的次数,小于0的时候变成0 然后比一下这两种的最小值. /*------------------------ ...

  8. [MCSM]Exponential family: 指数分布族

    Exponential family(指数分布族)是一个经常出现的概念,但是对其定义并不是特别的清晰,今天好好看了看WIKI上的内容,有了一个大致的了解,先和大家分享下.本文基本是WIKI上部分内容的 ...

  9. 运用Java对微信公众平台二次开发技术——开发者模式接入

    当初我在这碰到了很多问题,市面上以及网络上的资料特别少,所以当初碰了很多壁,所以现在跟大家分享一下,如何用Java,对微信公众平台进行二次开发. 一.开发预备知识: 最基本的JavaSE与JavaWe ...

  10. 【NDK开发】使用NDK开发android

    今天学习了一下android NDK,所以记录下来.据说NDK从r7开始自带编译器,在windows上无需配置cygwin的环境.现在我使用NDK r10来开发. 上午搭建的NDK并写了一个实例,不过 ...