本期精读的文章是:API设计原则
优秀的 API 之于代码,就如良好内涵对于每个人。好的 API 不但利于使用者理解,开发时也会事半功倍,后期维护更是顺风顺水。
一个骨灰级资深的同事跟我说过,任何在成长的代码库,至少半年到一年就要重构一次,否则失去的不仅是活力,更失去了可维护性与可用性。
由于本文已经是翻译后的文章,概要只列出不涉及 c++ 概念的思路框架,细节请移步译文。
极简且完备、语义清晰简单、符合直觉、易于记忆和引导API使用者写出可读代码。
尽量减少继承,让相似的类具备相似的 API,而不是统一继承一个父类。因为统一继承会带来 API public 数量过多,父级无意义的方法对用户产生误导。
属性指的是对象状态,通过属性为粒度的 API,有利于使用者理解 API 的含义,但需注意关联属性的顺序性。
比如传值 -1 的含义是什么?如果 API 文档不像 http status codes 一样健全,建议通过枚举的方式增加可读性。
不要使用缩写,保持一致性。类命名以功能分组作为后缀,比零散命名更易懂。
函数命名要体现出是否包含副作用,参数过多时以对象作为传参,布尔参数改为枚举类型,或者分解为两个语义化 API。
以下精读是对原文观点的补充。
eslint 有一条规则,不要直接改变入参的值。这个规则的初衷是解决函数副作用问题,禁止可能产生副作用代码的产生。但却可以通过如下方式避免:
function (num) {
let scopeNum = num
scopeNum = 5
}
这是从包含指针类型编程语言学习过来的,因为当 *num
表示指针时,代表代码可能产生副作用(修改入参的风险)。而 js 并不总是这样的,不但没有指针申明,基本类型也总是通过拷贝进入传参,非基本类型通过引用传递,也就是会发生通过如上代码绕过检测,却依然产生副作用(改变函数入参)的情况。
为了避免副作用,建议引入 flow
或 typescript
,通过 const
关键字与约定约束入参行为:
function (const num) {
...
}
将没有副作用函数的所有入参定义为 const
类型,静态检查阶段就禁止了对值的直接修改,同时因为有这个关键字的约束,在函数体内也约定不要通过引用浅拷贝修改它的值。
但这也无法彻底避免,仍然可以通过如下写法绕过检测,修改入参:
function (const num) {
const scopeNum = { ...num }
scopeNum.a.b = 'c'
}
在 js 中没有完美的方式避免对入参的修改,但通过对入参修饰 const
关键字,可以对使用者明确这是纯函数,对开发者提示不要写有副作用的代码。
c++ 的
const
定义从编译开始就完全杜绝了修改的可能性,虽然有const_cast
“去”const
行为,但仍然不会改变入参的值(虽然可以后续对值修改,指针指向保持不变,但用const
修饰的入参值永远不会改变)。
所有api定义之前,先抽离业务和功能语义的关键字,统一关键字库; 可以更好的让多人协作看起来如出一辙, 而且关键字库 更能够让调用者感觉到 符合直觉、语义清晰; 关键字库也是项目组新同学 PREDO 的内容之一, 很有带入感;
接口设计尽量要做到 单一职责,最细粒度化; 可以使用组合的方式把多个解耦的单个接口组合在一起作为一个大的功能项接口; 接口设计的单一职责,也更方便多人协作时候的扩展和组合;
对于接口参数的扩展,我们要做到面向扩展开放,面向修改关闭; 升级做到要兼容,否则会导致大批量的下游不可用。
同时也要避免过度设计,当抽象功能只有一处使用时,尽量不要过早抽象。
class User {
// good
setName() {}
// bad
setUserName() {}
}
在有上下文环境的调用中,减少不必要的描述可以提高 API 的精简和清晰度。
同时要避免过度使用解构,因为解构会丢失上下文,让我们对变量来源一无所知:
const { setName } = this.props.store.user
const { setVisible } = this.props.store.article
上述 setName
setVisible
脱离了 user
article
作用域,当隔着几百行调用时,早已不知所云。
参考优秀类库是设计 API 很好的方法之一,比如本文 c++ 参考的 Qt、js 可以参考 jQuery。
当 API 稳定后,需要花时间整理文档,因为写文档的思考过程可能推动着你重构和优化代码。
最后,如果有精力,最好每半年重构一次(然后完整跑一遍测试)!
如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。