迷思
首发于迷思
谈谈编译和运行

谈谈编译和运行

[作者按] 今天(3/21) hacker news 爆炸性的新闻是我们敬爱的葛老头:Andy Grove 去了。70后,80后大多听过这个响当当的名字,也听过(或者读过)『只有偏执狂才能生存』这本书。在90年代,葛鲁夫和盖茨一样,基本上等同于他们所缔造的王国,以及他们创立的 Wintel 联盟。如今90后创业者们所津津乐道的所谓创业大师们总结的:专注独特10倍速因子等等其实是拾了老爷子牙慧的。老爷子还有一本据说更好的书:High output management ,Ben(a16z 的合伙人)对其大为推崇。后知后觉的我没有读过,赶紧去图书馆 hold 了一本。去年,老爷子参加了 Ben 主持的一个颁奖会,看他几乎站都站不稳,话都快说不清楚的老态让我心酸,接着,听到他颤巍巍说出这句话,我几乎都要泪奔了:

Let's remember that millions of young people who have had the misfortune of being born in the wrong national boundaries are going through all the horrors that Ben described. I made it. Let's try in a little way to help them make it.

老爷子此时已经病入膏肓,无药可医,但依然心系那些和他有着类似磨难的年轻人。这便是范文正所谓的「不以物喜,不以己悲,居庙堂之高则忧其民;处江湖之远则忧其君」的写照。

葛老师一路走好。

。。。

言归正传。本来今天要讲讲 API 系统的配置和 CLI 等子系统,但是很多同学对我上一篇文章 再谈 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 的实现就是如此:

function route(params) {
  const result = joi.validate(params, routeSchema);
  if (result.error) throw result.error;

  const value = result.value;

  value.tag = helper.app.getAppTag();
  ...
  value.description = mustache.render(value.description, helper.doc);
  registeredRoutes.push(value);
}

每当用户撰写一个 route 的时候,我们实际上在往一个 list 里 push 这个 route 的数据。这个 list 究竟怎么用,是生成 restify 的 route,还是生成 hapi 的 route,我们在编译时再具体决定。这便是 framework-independent。

那么,什么是编译时,什么又是运行时呢?

const app = new Application();
app.compile(); // call app.setupRoutes(); etc.
app.run();  // call app.listen(); etc.

就这么简单。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 是逻辑上单独存在,未必需要物理上完全分离。

把「编译时」和「运行时」分离,是一项很重要的抽象能力。

文章被以下专栏收录