API的设计与实现

关于API的设计与实现

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乐观者在灾祸中看到机会;悲观者在机会中看到灾祸

API的设计与实现

相关文章:

你感兴趣的文章:

标签云: