在上一篇谈 API 的撰写 - 架构 文章里讲到:
通过这样一个接口,我们把 API 系统区隔为「编译时」和「运行时」。这个接口写出来的 API,更像是一个等待编译的源文件。在 API 系统启动的时候,会经历一个「编译」的过程,把所有的
route
汇总起来,生成 restify 认识的路由形式,同时,收集里面的各种信息(比如 validator,authentication),供框架的各个 middleware 使用。
「编译」(compile)是软件系统的一个非常非常重要的概念;很可惜,在 python / ruby / javascript 等解释型语言大行其道的当下,很多人已经不知道编译为何物。当然,即便你使用 c / go / java 等编译型语言,有多少人又真正清楚「编译」究竟是个什么过程呢?
在 wikipedia,compile / compiler 的解释如下:
A compiler is a computer program (or a set of programs) that transforms source code written in a programming language (the source language) into another computer language (the target language), with the latter often having a binary form known as object code.[1] The most common reason for converting source code is to create an executable program.
所以 compile 实际上是一种 transformation(我们又见到这个词了):它把某个数据(如果你认为源代码也是一种数据的话)从一种格式转换成另外一种格式。在编译型语言里,这种转换是为了生成机器码(如 c / go),或者 byte code(java / c#),方便机器执行(byte code 会进一步以 JIT 的方式 compile 成机器码)。
(题外话:其实解释型语言也是有一个 JIT 「编译」的过程;现在纯粹的,完全在运行时一句句解释执行的语言,只能生存在象牙塔里)
有了这样一层(indirection)编译的过程,源数据和目标数据就被分离开,可以做很多事情,比如 wikipeidia 上说的 compiler 的一大功效:
Compilers enabled the development of programs that are machine-independent.
这里面,这个 machine-independent 可以根据你的需要被换成 framework-independent,甚至 language-independent。
那么,一份源代码除了可以生成目标代码(主产品)外,还能有什么副产品?我们以 java 为例:
- 如果你的注释遵循 javadoc,那么从代码里可以生产出来漂亮的文档(SDK)。
- facebook/infer 可以对你的代码做详细的 static analysis。
- jacoco 可以根据源码和 test case 生成 coverage report。
- ...
这些副产品带啦的好处是显而易见的:我们不用为了一些特定的目的而做一些额外的事情。
回到我们说的 API 系统。我提到了这样的一个接口:
你可以将其看做是一段声明 API 的代码,但我更愿意将其看做是一段描述 API 的数据。这个数据有:
- method:API 使用何种 http 方法调用。
- path:API 使用什么样的 endpoint。
- description:API 的文档。
- validators:如果要验证 API 的输入数据,如何验证。
- action:API 具体做些什么事情。
- flags:API 有哪些属性(需不需要验证,支不支持某些特定的操作等)。
如果你以数据的眼光看待这段代码,那么,每一个 route()
的声明都可以被聚合起来,放到一个数组里。事实上,route
的实现就是如此:
每当用户撰写一个 route
的时候,我们实际上在往一个 list 里 push 这个 route
的数据。这个 list 究竟怎么用,是生成 restify 的 route,还是生成 hapi 的 route,我们在编译时再具体决定。这便是 framework-independent。
那么,什么是编译时,什么又是运行时呢?
就这么简单。app.compile()
把放在 route list 里面的数据转换成 restify 的 route,而 app.run()
开始进行网络监听。很多同学看到这里会想,有没有搞错,我还以为是什么高深的东西呢,这代码我也会写啊。的确,这里没有任何高深的东西。然而,关键的是你会不会想到把一段代码的运行分解成:compile()
和run()
两个阶段。只有你这么去想了,你才会反过来考虑你的 API 的代码能不能退一步,用一个数据结构封装,你才会想这个数据结构该如何设置,你才会把 route()
的实现写成类似的方式。
在「编译时」你可以做很多繁杂的事情,就像高手过招前先养气御剑一样;这样,在「运行时」,你才能打出行云流水的招式。
再举一个例子。就写 blog 而言,你可以用 wordpress,也可用 jekyll 这样的 static site generator。前者把编译和运行混在一起,在请求页面的时候生成博文;而后者则将二者完全分离,你得使用 jekyll 的工具把 markdown 撰写的博文编译成 html,才能被正常访问。这样分离之后,天地开阔了很多,你可以在「编译时」为所有文章生成全文搜索所用的索引,可以根据文章的类别 / tag 生成目录,相关文章,菜单等等,在为运行时提供了闪电般的速度外,还能提供 wordpress 才能提供的动态性和灵活性。
注意,这里所说的分离完全是逻辑上的分离,就像上一篇文章中的 pipeline,每个 component 是逻辑上单独存在,未必需要物理上完全分离。
把「编译时」和「运行时」分离,是一项很重要的抽象能力。