浅谈API设计

时间:2022-03-25 23:16:55

为什么需要了解一些API设计?
只要你编程,你就是API Designer
一个好的设计,模块之间的耦合应该也是API级别的
一个程序,如果你独立开发,那你既是API的Designer,也是API的User
如果你和你的同事一起开发,,你既是你开发的模块API的Designer,也是其他同事模块API的User

一个好的API应该具备哪些特点?
1. 易学易用(Easy to learn and use)
要做到易学易用,需要满足以下基本要求:
a. API命名的要适应用户的习惯并遵循一些模式。例如:使用类似于add/delete, get/set,push_bach/pop_back这种大家比较熟悉的命名方法。避免用户使用get获取一个数据,但是需要使用insert插入数据。
b. API的设计需要尽量简化用户的使用复杂度,我们看下STL vector的使用,非常简洁!

// vector::push_back
#include <iostream>
#include <vector> int main ()
{
std::vector<int> myvector;
int myint; std::cout << "Please enter some integers (enter 0 to end):\n"; do {
std::cin >> myint;
myvector.push_back (myint);
} while (myint); std::cout << "myvector stores " << int(myvector.size()) << " numbers.\n"; return 0;
}

c.尽量提供一些方便用户的api方法
例如提供一个STL vector提供at方法供用户获取指定位置的元素

reference at (size_type n);

但同时也提供API供用户直接获取第一个和最后一个元素

reference front();
reference back();

2. 引导用户写出可读性高的代码(Leads to readable code)
好的API设计,可以引导用户写出高可读性的代码,下面这个例子非常生动:

实现 1:

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

实现 2:

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

显然第二种设计更易引导用户写出可读性高的代码

3. 很难被误用
一个好的API设计,会使用户很容易写出正确的代码,而不是错误的代码。
这点非常重要,如果你的API很容易用错,那么,一方面用户会吐槽,另一方面,API的维护成本也很高
so,当你收到很多错误报告的时候,不要抱怨用户的使用方法有问题,review下自己的API,是不是很容易被误用
4. 方便扩展(Easy to extend)
api会变的越来越“大”,未来,api会提供新的类,类中会有新的方法,方法会有新的参数,枚举数据也会有新的枚举值。
因此,在API设计过程中,需要时刻提醒自己保证API的可扩展性
5. 完整(Complete)
所谓的完整,并不是指API满足用户所有的功能需求,而是说,基于这些API,可以满足所有功能需求。
我们还是拿STL::Vector来做说明,从获取数据的角度,API提供了reference at (size_type n); 就是完整的,另外提供的front back都是方便用户获取数据,用户完全可以个性化实现这样的功能。但是,如果只提供了front、back,而没有提供at这样的api,就是不完整的

兼容性
在设计API时,非常重要的点是要提前考虑API的发布方式,在发布过程中,兼容性非常重要
我们可以将兼容性分为:源代码兼容,二进制兼容,功能兼容
在Deliver新版本的API时,需要明确告知用户,API版本在哪些层面做到了向后兼容
源代码向后兼容:老的程序,使用新的API库依然能够正常工作
二进制向后兼容:直接升级库文件,老的程序不必重新编译就能使用新的库文件
功能兼容向后兼容:老的程序,使用新的API库,不影响已有功能

功能兼容是业务的范畴,这里不展开。着重讨论下源代码兼容和二兼职兼容。一个很有意思的讨论Source compatible vs. Binary compatible
一般来说,二进制兼容是源代码兼容的子集。如果你的代码打破了源代码兼容,基本上可以肯定你也打破了二进制兼容。相反则不成立,一个API是源代码兼容但不是二进制兼容表示你的代码无需如何修改,只要重新编译程序即能正常工作。

我们看下c++来,什么情况下我们会满足源代码兼容,但不满足二进制兼容
c++发布api有静态和动态库两种方式,这两种发布方式各有优劣,当采取动态库的发布方式,就面临二进制兼容问题
c++通过头文件使用动态库,编译时也据此产生二进制代码,因此,在考虑动态库兼容性的时候,只要考虑库使用已有的头文件是否和新的动态库兼容
c++使用动态库最常见的二进制兼容问题就是使用虚函数作为接口产生的。c++虚函数通过虚函数表实现,虚函数表具有以下特点:
1)虚函数按照声明顺序放在表中
2)父类的虚函数放在子类的虚函数之前
so,如果我们在新版本的动态库中插入新的接口定义,由于虚函数的顺序,导致二进制兼容问题
即使你在你的接口中将新增虚函数放在最后,由于接口可能被继承,而父类的虚函数在子类虚函数之前,因此还是会导致二进制兼容性问题
为了解决这个问题,我们可以采用pimpl来解决!
有一篇非常棒的文章,C++ 工程实践(5):避免使用虚函数作为库的接口

一些API开发的建议:
1. 践行TDD
2. 如果可能,尽早和API使用方沟通
3. 重视命名,它比你想象的更重要

Reference:

Joshua Bloch:How to Design a Good API and Why it matters
Jasmin Blanchette Trolltech :The Little Manual of API Design
Kinds of Compatibility: Source, Binary, and Behavioral