View on GitHub

modularization-examples

代码防腐实用技术

通过多进程,我们可以实现业务逻辑的动态组合。但是用普通函数也可以组合

function f(g: () => void) {
    // do something
    g();
    // do something else
}

这样 f 和 g 就组合起来了,我们称 g 为一个插件。 插件边界是指当 f 和 g 来自于不同的 git 仓库的时候,f 调用 g 就是跨越了 git 仓库的边界。 那怎么能实现 f 和 g 来自不同的 git 仓库呢?

一些运行时环境,例如 iOS 是不鼓励从网络动态加载代码的。 插件并不一定等于动态化,插件完全可以是静态装配的。

无论代码是怎么组合成运行时的进程整体的,拼装后总是有拼接缝的。 那我们就可以在“插件loader”这样的地方去添加打日志与监控的代码。 并不是 RPC 调用才有错误率,请求延迟这些指标。插件也可以有。

虽然函数调用的频次是非常高,暴力记录所有的函数调用是吃不消的。 但是我们完全不用关心 Array.map,Set.add 这样的函数被调用了多少次, 只需要区分好哪些是插件,然后把这些高价值的插件函数给监控好就足够提供 Feedback 了。 不同 git 仓库的所有者,仍然可以从日志里拿到自己的日志,和进程隔离没有区别。

如果有强制的插件规范,那甚至可以做到“内存状态”的隔离,用编译检查等手段禁止全局变量,禁止偷偷地访问另外一个插件的内部状态。 一个进程要读取一份配置,只有第一次RPC的时候我们才能监控到,后续缓存在内存里的访问就是不可见的了。 而插件则不用担心跨插件调用的开销,我们可以把配置缓存在另外一个插件里,这样每次读取配置都是跨插件的调用,从而可以被监控到。

插件和进程的核心区别就是插件可以适用于更多的运行时(比如iOS,微信小程序),可以用于拆分运行时调用关系更频繁更紧密的界面和流程。 进程边界的优势来自于社区共识,提前提供了大量开源的优秀基础设施。 但是并不意味着,除了进程边界,我们不能在进程内再为每个 git 仓库划出边界来,只是要付出一些自研代价罢了。 社区共识是不够的,在函数边界(特别是同步调用栈之外的其他调用关系),以及插件边界上都没有足够强的规范。 只要能在公司或者部门级别建立好共识,函数边界和插件边界完全可以满足问题定位的需求,甚至比进程边界做得更好。

那么真正的障碍是什么?是变更自主性,变更的粒度和权责问题。这部分在控制变更里讨论。