http://blog.csdn.net/horkychen/article/details/46612899php
API的设计是软件开发中一个独特的领域。最主要的特殊点在于API是供开发者使用的界面,即Application Programmer Interfaces。相似于用户能够直接使用到的GUI的做用同样。因此相对于依据软件设计的原则,考虑用户的”体验”会更加剧要。程序员
许多著名的工具和库的做者都写过相关的著做,详细的论述他们在API上的设计与实现要点。下面的论述,就是从这些前人的工做成果中总结而来。如下先列出参考资料:算法
狭义上API可能只是一个动态库(共享库)提供功能的接口定义。广义上API分为public API,以及internal API之分。既有总体软件系统对外输出的接口(包括与设备通信的接口),也有系统内一个底层模块提供给上层模块使用的接口定义。编程
API看似简单的名词,却表明着重要的架构设计。从架构设计的角度来看(所谓的组成论),软件系统就是模块和接口。模块(层次/组件)决定分工,接口决定交互。API就是接口的定义。模块间并不须要关心其它模块的实现,只须要了解如何进行协做便可。这样将复杂度分散到各个模块之中,使得总体系统更为可控。而API的本质,就是提供给模块开发者使用的接口,是给”人(Programmer)”用的。API的设计任务的核心就是保证使用者以较低的成本,正确的使用接口,驱动模块完成他们的业务。对于Public API,最大的设计挑战则是如何把API一次就作对!api
附1的做者在书中提到了一个”无绪(cluelessness)”的概念,即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设计者,不能假定使用者都会认真的看完全部的文档,而是要尽可能作到两点:框架
那些被公认的行为和命名就很是重要,千万不要作太多创新。请遵照最小惊喜原则。
这样有助于理解,也很难被误用。当一个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的设计实现,不一样的背景,不一样的需求会有不一样的描述了。我这里归纳了一些他们间相通的要点。
若是公开一个构造函数,那么建立的对象必定是类的实例。而工厂方法更具灵活性,虽然参数彻底相同,但能够返回一个子类的实例。同时更利于实现单例或者缓存对象实例。
在Chromium一些模块的接口上,经常能够看到这类的应用。
常量修饰符,有助于限定没必要要的修改动做,也是一种行为约定。不管是对参数,函数,或是返回值,均可以视须要添加常量修饰符。
相对于在建构时传入一串参数的接口类,不如在建构后再以setter设置其它参数的方式。其区别在于后者更利于编写可读性的代码。在上面关于可读性代码中已举过例子,这里再也不赘述。
要点是各个属性须要作到正交,且与顺序无关。
对因而否须要提供虚函数形式的API,也是一直有争论。这里并非讨论接口类(纯虚类)的定义,接口类的定义的必要性是明确的,不须要额外讨论。
原则上对虚函数做为API是限制使用的,缘由是继承下的override可能会致使接口的行为变得不符预期,由于子类的行为没法肯定。
但在一些场景下确实有必要为使用者提供必定的扩展性,就能够提供虚函数,以便使用者能够经过继承改变原来的行为。
以整型数据代替Enum的做法相似,关键在于使用者的理解。
能够改进的作法包括,分红不一样的函数实现,或者以枚举变量代替。
示例:
widget->repaint(); widget->repaint(true); widget->repaint(false);
分开函数的方式:
widget->repaint(); widget->repaintWithoutErasing();
使用整数代替格枚举变量时也是相同的问题。
在附5中做者详细说明了关于API中的异常处理。个人总结是只抛必须抛的异常,毫不能自做聪明的默默处理。API的代码应当最真实的反应出执行中的问题,更不能用聪明的代码作某些特别处理。其背后的缘由是这样作会使得API的行为与预期会发生误差,违背了最小惊喜原则。
在命名上,附2列举的比较详细。归纳以下:
一个模块(库)的兼容性主要包括:
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