大师网-带你快速走向大师之路 解决你在学习过程中的疑惑,带你快速进入大师之门。节省时间,提升效率

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。