美文网首页PHP Chaos
API 设计学习笔记

API 设计学习笔记

作者: xiaojianxu | 来源:发表于2017-06-15 17:53 被阅读12次

    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。

    相关文章

      网友评论

        本文标题:API 设计学习笔记

        本文链接:https://www.haomeiwen.com/subject/fcsuqxtx.html