API 设计学习笔记
Think about future, design with flexibility, but only implement for production.
API 设计
谈论 API 的设计时,不只局限于讨论“某个框架应该如何设计暴露出来的方法”。作为程序世界分治复杂逻辑的基本协作手段,广义的 API 设计涉及到日常开发中的方方面面。
最常见的 API 暴露途径是函数声明(Function Signiture),以及属性字段(Attributes);当涉及到前后端 IO 时,则需要关注通信接口的数据结构(JSON Schema);如果还有异步的通信,那么事件(Events)或消息(Message)如何设计也是个问题;甚至,依赖一个包(Package)的时候,包名本身就是接口。
好的 API 设计标准就是易用。
只要能够足够接近人类的日常语言和思维,并且不需要引发额外的大脑思考,那就是易用。
按照要求从低到高的顺序如下:
达标:词法和语法
- 正确拼写
- 准确用词
- 注意单复数
- 不要搞错词性
- 处理缩写
- 用对时态和语态
进阶:语义和可用性
- 单一职责
- 避免副作用
- 合理设计函数参数
- 合理运用函数重载
- 使返回值可预期
- 固化术语表
- 遵循一致的 API 风格
卓越:系统性和大局观
- 版本控制
- 确保向下兼容
- 设计扩展机制
- 控制 API 的抽象级别
- 收敛 API 集
- 发散 API 集
- 制定API 的支持策略
达标:词法和语法
正确拼写
准确用词
Message、notification、news、feed
准确地用词,从而让读者更易理解 API 的作用和上下文场景。
React.createClass({
getDefaultProps: function() {
},
getInitialState: function() {
}
});
props 是指 Element 的属性,要么是不存在某个属性值后来为它赋值,要么是存在属性的默认值后来将其覆盖。default 是合理的修饰词。
State 是整个 Component 状态机中的某一个特定状态,状态和状态之间是互相切换的关系。所以对于初始状态,用 initial 来修饰。
成对出现的正反义词不可混用
Show & hide/ open & close / in & off / previous & next / forward & backward/ success & failure...
注意单复数
数组(Array)、集合(Collection)、列表(List)这样的数据结构,在命名时都要使用复数形式:
var shopItems = [];
export function getShopItems() {
// return an array
}
注意,复数的风格上保持一致,要么所有都是 -s,要么所有都是 -list。
在涉及到诸如字典(Dictionary)、表(Map)的时候,不要使用复数。
不要搞错词性
分不清名词、动词、形容词......
成对出现的单词,其词性应该保持一致。
succeed & fail, success & failure
n. 名词:success, failure
v. 动词:succeed, fail
adj. 形容词: successful, failed(无形容词,以过去分词充当)
adv. 副词:successfully, fail to do sth (无副词,以不定式充当)
方法命名用动词、属性命名用动词、布尔值类型用形容词(或等价的表语)。但由于对某些单词的词性不熟悉,也会导致最终的 API 命名有问题。
处理缩写
首字母缩写词的所有字母均大写
export function getDOMNode() { }
用对时态和语态
调用 API 时一般类似于“调用一条指令”,所以在语法上,一个函数命名是祈使句式,时态使用一般现在时。
生命周期、事件节点,需要使用其他时态(进行时、过去时、将来时)。
Export function componenntWillMount() { }
Export function componentDidMount() { }
Export function componentWillUpdate() { }
Export function componentDidUpdate() { }
Export function componentWillUnmount() { }
生命周期节点(mount, update, unmount, ...)
采用 componentDidMount 这种过去时风格,而没使用 componentMounted ,从而跟 componentWillMount 形成对照组,方便记忆。
精细的事件切面,引入 before、after 这样的介词来简化:
// will render
Component.on('beforeRender', function() { });
// now rendering
Component.on('rendering', function() { });
// has rendered
Component.on('dataRender', function() { });
尽量避免使用被动语态,我们要将被动语态的 API 转换为主动语态。
// passive voice, make me confused
Object.beDoneSomethingBy(subject);
// active voice, much more clear now
Subject.doSomething(object);
进阶:语义和可用性
单一职责
具体业务逻辑中“职责”的划分
小到函数级别的 API,大到整个包,保持单一核心的职责都是很重要的一件事。
// fail
component.fetchDataAndRender(url, template);
// good
var data = component.fetchData(url);
component.render(data, template);
Class DataManager {
fetchData(url) { }
}
Class Component {
constructor() {
this.dataManager = new DataManager();
}
render(data, template) { }
}
文件曾面同样,一个文件只编写一个类。
避免副作用
主要指的是:1)函数本身的运行稳定可期;2)函数的运行不对外部环境造成意料外的污染。
对于无副作用的纯函数而言,输入同样的参数,执行后总能得到同样的结果,这种幂等性使得一个函数无论在什么上下文中运行、运行多少次,最后的结果总是可预期的。
// return x.x.x.1 while call it once
this.context.getSPM();
// return x.x.x.2 while call it twice
this.context.getSPM();
每次返回一个自增的 SPM D 位,但是这样子的实现方式与这个命名看似是幂等的 getter 型函数完全不匹配。
不改变函数内部的实现,而是将 API 改为 Generator 式的风格,如:SPMGenerator.next()。
对外部造成污染的两种途径:一是在函数体内部直接修改外部作用域的变量,甚至全局变量;二是通过修改实参间接影响到外部环境,如果实参是引用类型的数据结构。
防止副作用的产生,需要控制读写权限。比如:
- 模块沙箱机制,严格限定模块对外部作用域的修改;
- 对关键成员作访问控制(access control),冻结写权限等。
合理设计函数参数
函数签名(Function Signature)比函数体本身更重要。函数名、参数设置、返回值类型,这三要素构成了完整的函数签名。其中,参数设置是使用得最频繁的。
如何优雅地设计函数的入口参数呢?
第一、优化参数顺序。相关性越高的参数越要前置。
相关性越高的参数越重要,越要在前面出现。可省略的参数后置,以及为可省略的参数设定缺省值。
第二、控制参数个数。用户记不住过多的入口参数。
参数能省则省,或更进一步,合并同类型的参数。
JS 中的 Object 复合数据结构
// traditional
$.ajax(url, params, success);
// or
$.ajax({
url,
params,
success,
failure
});
好处是:1)记住参数名,不用关心参数顺序;2)不必担心参数列表过长。将参数合并为字典这种结构后,想增加多少参数都可以,也不用关心需要将哪些可省略的参数后置的问题。
劣势是,无法突出哪些是最核心的参数信息;设置参数的默认值,会比参数列表的形式更繁琐。
兼顾地使用最优的办法来设计函数参数,目的是易用。
合理运用函数重载
在合适的时机重载,否则宁愿选择“函数名结构相同的多个函数”。
Element getElementById(String: id)
HTMLCollection getElementsByClassName(String: names)
HTMLCollection getElementsByTagName(String: name)
对于强类型语言来说,参数类型和顺序、返回值通通一样的情况下,压根无法重载。
关于 getElements 那三个 API,最终的进化版本回到了同一个函数:querySelector(selectors);
使返回值可预期
函数的易用性体现在两方面:入口和出口。出口,即函数返回值。
对于 getter 型的函数来说,调用的直接目的是为了获得返回值。
让返回值的类型和函数名的期望保持一致。
// expect 'a.b.c.d'
Function getSPMInString() {
// fail
return {a, b, c, d};
}
而对于 setter 型的函数,调用的期望是执行一系列的指令,去达到一些副作用,比如存文件、改写变量值等等。因此,绝大多数情况选择了返回 undefined / void , 这并不是最好的选择。
我们在调用操作西戎的命令时,系统总会返回 “exist code”,这样子能够获知系统命令的执行结构如何,不必校验“这个操作到底生效了没”。因此,创建这样一种返回值风格,或可一定程度增加健壮性。
另一选项,让 setter 型 API 始终返回 this,来产生一种“链式调用(chaining)”的风格,简化代码且增加可读性:
$('div')
.attr('foo', 'bar')
.data('hello', 'world')
.on('click', function() {});
固化术语表
为了避免相似的词,被混用,最终给系统引入问题。
一开始就要产出术语表,包括对缩写词的大小写如何处理,是否有自定义的缩写词等等。一个术语表可以形如:
| 标准术语 | 含义 | 禁用的非标准词 |
pic 图片 image, picture
path 路径 URL,url, uri
off 解绑事件 unbind, removeEventListener
emit 触发事件 fire, trigger
module 模块 mod
不仅在公开的 API 中要遵守术语表规范,在局部变量甚至字符串中都最好按照术语表来。
对于一些创造出来的、业务特色的词汇,如果不能用英语简明地翻译,就直接用拼音:淘宝 taobao,微淘 weitao,极有家 jiyoujia 。
遵循一致的 API 风格
词法、语法、语义中都指向同一个要点:一致性。
一致性可以最大程度降低信息熵。
一致性大大降低用户的学习成本,并对 API 产生准确的预期。
- 在词法上,提炼术语表,全局保持一致的用词,避免出现不同的但是含义相近的词。
- 在语法上,遵循统一的语法结构(主谓宾顺序、主被动语态),避免天马行空的造句。
- 在语义上,合理运用函数的重载,提供可预期的,甚至一类类型的函数入口和出口。
具体例子:
- 打 log 要么都用中文,要么都用英文。
- 异步接口要么都用回调,要么都改成 Promise。
- 事件机制只能选择其一:object.onDoSomething = func 或 object.on('doSomething', func)。
- 所有的 setter 操作返回 this。