本来想写一个怎么写好代码的主题,昨天突然看到了淘宝团队的这篇文章《编写「可读」代码的实践》,把我想到的没想到的基本都说了。然后我就想重复的内容就不说了(别人讲的比我透彻),我自己就补充下自己对于接口几点想法,然后标题呢,我就死皮赖脸的抱下大腿了。
修改接口
一般来说,对于现有系统我们原则上不主张修改已有接口,如果原有接口有问题或者没有办法满足要求了,那么我们会新增接口,然后在调用的地方替换掉老接口。如果直接修改现有接口会影响到所有调用此接口的地方,一旦修改后的接口出现问题,那么将影响整个系统的稳定性。而新增接口,如果出现问题只要通过简单的修改调用处的代码即可回退。如果一定要修改老接口,那么要遵循一定的原则:
1. 输入输出不能有变化(此时有单元测试的好处就体现出来了)。
2. 接口的行为不能有变化,同步的接口不能变成异步,反之亦然,原来不抛异常的不能变成抛异常。
接口实现要高内聚
这里高内聚主要谈的是接口要把自身的异常或者差异化在本身的实现中处理掉,不能把自身的问题传导到接口之外。譬如说前端通过浏览器获得的 language code,可能大小写和格式因每个浏览器的实现有差异化,那么我们封装一个接口 getLanguageCode 就要把这个差异化在接口内处理掉,保证这个接口返回的结果是标准化的,调用者拿到这个返回值在传给其他模块甚至后台时一定是可预测的值(符合规范)。如果你把自身的差异化扩散到其他接口,模块或者系统,带来得就不单单是可维护性的风险:
1. 其他系统的维护者不一定明白为什么对这种情况需要做特殊处理,增加了维护的难度。
2. 即使其他系统对你的输出做了特殊处理,但是一旦你的异常或者差异化变化了,那么修改会涉及到多个系统,整个系统的稳定性就降低了。
3. 最坏的情况是某个系统的异常情况处理传播到了整个系统链的多个环节,一旦出现问题,非常难追查源头。
接口输出的一致性
笼统的说就是方法的返回类型最好能保持一致,即缺省的状态下和非缺省的状态下返回值的类型要一致,譬如getUsername,如果能获取到用户名则返回一个 string 的用户名,如果允许用户不填用户名,则此时调用 getUsername 应该返回空字符串 '',而不是 undefined 或者 null。这点在前端也是非常好用,通常获取到字符串后都需要在界面上展示,如果缺省返回 undefined,则需要再增加一次判断,当 undefined 的时候在页面上展示空字符串,如果返回类型一致就不需要这一步。
原始类型的返回值比较好统一,非原始类型就需要分情况讨论了。
第一种情况,返回类型的对象表示的是数据结构,譬如系统定义了用户可以有多个联系方式,那么 getUserContact 定义为返回一个数组,数组项是用户的填写的手机号,当用户未填或者查询不到手机号时,getUserContact 应该返回空数据 [] 而不是 undefined。这么做的好处是,一般调用返回数据结构的接口后,都需要对数据结构进行操作,比如遍历,筛选。如果返回类型一致就不需要对是否异常情况进行判断(调用者不会迷惑说到底没有联系方式返回的结果是 undefined 还是 null 还是 [],对调用者友好)。
第二种情况,返回的对象本身表示的是一个 domain object,譬如获取账户信息 getUserAccountInfo,返回的应该是一个 AccountInfo 的 domain object,那么如果用户没有填写或者获取不到,返回应该是一个 null,而不是一个空对象 {}。因为返回值本身是否为 null 是有现实意义的,表示是否存在对应对象。和第一种情况的区别是,第一种情况数据如果不存在是通过数据项来表示的(array.length===0, map.entries().length===0),数据结构本身是不应该变化的。这里为什么选择 null 而不是 undefined,也是想把没有这个对象和_未定义这个对象_这两种情况区分开来,当然这个约定不是强制的。
接口输出一致性原则我们在 js 的众多 api 中都能找到很好的体现,譬如 Array.prototype.findIndex 如果没有找到 index 则返回的是-1,譬如 Array.prototype.filter 如果过滤不出符合条件的项目则返回的是空数组。譬如 Array.prototype.find 如果找到到符合条件的则返回 undefined。