McFog@がんばらない

一种API代码结构的设计思路

2015-02-06

一种API代码结构的设计思路

2015-02-06

Twitter’s API Playground

Prologue

在写API的过程中有这样三种产物

  • 文档

    几乎没人爱写文档,写了也懒得维护。可是同时API的文档对于其他程序员来说又是赖以生存的必需品。因此大家对文档都是爱恨交加,恨自己要维护文档,爱别人写好的漂亮文档;恨别人的烂文档错文档,爱自己随便乱写乃至不写文档。

  • 操场

    比文档更高一个层次的奢侈品,不用写代码简单点点或者repl形式马上就能探索接口的行为。不像仅对别人,操场的存在对自己开发接口也是很有帮助的。

    没操场的时候,大家往往人肉建一个文件当操场用,在里面各种玩API,但使着编译型语言的兄弟们没那么好运,他们更需要有操场。

    当然如果实践TDD或BDD的话,用例大概能代替操场一部分的作用,但这只对写接口的人管用。对调用接口的人来说,操场的作用是难以取代的。

  • 字段校验逻辑

    自己写接口的时候必须要做的事情。没做那是100%bug。

想想这三个产物的共同特点是什么?插入一段广告,哦不,我先把他们捏成一张表

别人的接口 自己的接口
文档 没有会死 没用,不想写
操场 超级提升幸福感 想用,懒得写
字段校验 不太关心 必须写

他们的共同特点是均源自接口输入的schema

大家为啥不喜欢写文档?为啥懒得实现操场?我觉得很可能源自_程序员的直觉_。程序员天生讨厌重复,他们知道重复意味着修改的时候要同步维护,意味着大量重复的劳动和不同步带来的各种巨坑,**Duplicate Is Evil**。**DUPLICATE IS EVIL**

换个角度说,怎么诱使程序员乖乖地写文档呢?首先利诱“写文档的话送你个操场玩玩”,然后威逼“反正你也得校验字段,咱们校验字段的方式就是写文档”,最后再给个枣子“这文档不用维护,以后改程序的同时文档自动一起改”

前言到此为止,结论只有一个:

我们要让文档、操场、字段校验三位一体!让不写文档比写文档难!让所有接口都有100%准确的文档和操场!消灭天下没文档的接口!消灭一切和实际接口不符的文档!

具体实现的思路其实刚才已经偷偷提到了,那就是聚焦接口的__输入schema__,以schema作为数据源,文档、操场、字段校验是schema的3种不同的应用。所以我们要构建的就是 a)meta-schema,即如何定义schema b)如何通过schema生成3种不同的产物。

Meta-Schema

程序员做事大体上有两种选择:找现成的轮子削成方的来用或自己造个方的轮子。我们想要的轮子“方”在哪里呢?嗯,schema大家都拿来做校验,但我们希望不仅能校验,还能生成文档和操场,也就是说希望它能够内嵌一些描述文字,最好校验规则也能轻松转换成文字展示。

自己造轮子的话,下面是一个基本的结构可以参考

class Schema {
    string uri;//接口位置/调用时的名称/url等
    string name;//接口中文名
    string desc;//说明
    array fields;//接口字段
}

class Field {
    string key;//字段key
    string desc;//说明
    bool required;//是否必填
    array constraints;//字段约束
}

class Constraint {
    bool check(value);//校验
    string describe();//描述
}

//Constraint常见的子类有:类型校验,长度/范围校验,正则校验等

找轮子的话,我用过json-schema做这一块,能用,很多语言都有校验的实现,还有类似json-editor这样的web editor实现,操场比较好搞。大体上能内嵌说明文字,各种语言都有良好实现,数据结构清晰容易导入导出的schema项目都可以用。

DSL for Meta-Schema

“好麻烦,我还是if if if吧”

只有schema还不够,你会需要负责教会所有同事一门新手艺,这可能将我们的努力毁于一旦,自己用起来也不爽。我们要的是DSL,要IDE的代码提示,要code-as-configuration。

怎么实现DSL超出本文范围,有机会我会再写这方面的东西,js的话大概300行代码就能实现类似这样的dsl了

    legate().define
        .name('测试接口')
        .desc('演示API用的测试接口')
        .cmd('test')
        .param
            .int('num', '一个数字', [3, 100])
            .string('str', '字符串', [2, 5])
            .regex('reg', '有追求的字符串', /a.+b/)
        .endParam
        .logic(function(param) {
            return {
                result: 'from remote',
                param: param
            };
        })
    .endDefine
    .mount(legateServer);

有了DSL,基本上已经可以宣告不写文档比写文档难了,下面要给按我们的meta-schema做事的乖孩子说好的三大产物。

显灵

其实原本写到这里就可以收笔了,因为定义好了数据结构,写写视图对程序员来说实在没啥挑战,不是么?

文档就是

loop schemas schema
  show schema
  loop schema.fields field
    show field

操场就是文档套上

<form target="result">
</form>
<iframe id="result">

校验就是

loop schema.fields field
  if no $data[field]
    if field.required
      BIG-BANG!
    else
      continue

  loop field.constraints constraint
    constraint.check $data[field]

啊,不小心还顺便实现了分发或者说路由呢

loop schemas schema
  if $request match schema.uri
    return run schema, $request

NOT-FOUND!

是不是根本停不下来?

另一个好消息是,这个思路下各种其他开源项目都很容易拿过来套上,比如之前提过的json-schemajson-editor,比如最近的swagger.io等等,只要把握住核心是schema,操场/文档/校验逻辑等都是schema的视图,世界便尽在我们的掌握之中。

附《The Pragmatic Programmer》中的相关tips

[11] Don’t Repeat Yourself
[12] Make It Easy to Reuse
[17] Program Close to the Problem domain
[68] Build Documentation In, Don’t Bolt It On

© 2020 McFog W. All rights reserved. がんばらない