背景
随着系统老化、开发工具逐渐落伍、bug 堆积,项目会变得及难维护。所以“腐烂”,是所有遗产项目不可避免的一环。一般企业基本不会再去碰遗产项目,但是现代很多公司却喜欢另辟蹊径——每两三年用新技术重构一遍代码。众所周知,遗产代码很难改动,那它们这么做的自信来自何处?
故事要从一个叫 Martin Fowler 的老头说起。有一次,他在热带雨林里旅游,无意间发现了一种叫“绞杀者藤蔓”的植物:
绞杀者藤蔓会沿着其他树干一路向上生长,以获取光线资源;随着藤蔓的不断生长,之前的寄生树会被这种植物整个包裹,并随着阳光资源的枯竭而死亡;最后树木腐烂而逝,但是原地却留下了一颗树状的巨大藤蔓。
Martin 老头回去后就把这个故事写在了博客上,并提出了一个叫“绞杀者模式”的策略:通过逐步重构单体应用(而不是推到重来的方式),逐渐构建出一个新的应用程序。这也成为了后来开发人员对遗产系统进行现代化改造的基本方针。
绞杀者模式
讲完植物,我们从实现策略上探讨一下“如何绞杀”;绞杀的大体操作如下:
- 创建一个门面拦截后端遗产系统的请求
- 通过一定的规则,门面会将特定请求分别路由到新旧系统上——反向代理
- 保留遗产系统的原有功能,逐步向新系统中迁移功能模块;由于门面的隔绝,消费者照常使用现有功能,并不会有任何迁移的感知
- 完成所有迁移,“绞死”遗产系统
绞杀者的好处就是,保留了遗产系统的代码,以渐进的形式朝着新应用迈进;这保证了每一步前进都有回退的机会。渐进迁移的时间可能会很长,甚至可以永久性的保持部分遗产系统的功能。
实操
OK,理论很简单,实操中会有各种各样的问题,重构过程通常要持续数月甚至数年。简单宣称“通过绞杀者模式可以实现系统迁移”,这种话是没用的:一想到你要动所有代码,大家立马就慌了,决策者(不管懂不懂技术)都不会如此草率地行动。所以最好能有更细节的拆分步骤。所谓的“拆分”,可以从表现层、服务层、持久层等等方面入手;只要记住从最简单的部分开始,当显现出价值后,就可以持续加速改造了。
通常来说,表现层的拆分是最简单的,比如将 JSP 或 tymeleaf 这种后端渲染框架拆成 Json api + react 的形式,就可以很快出货。我们看看具体的改造过程。
反向代理
实现绞杀者模式的第一步自然是加个门面啦。我能想到最最最简单的门面就是:专职的反向代理工具 nginx 了。反向代理我之前写过一篇文章,大家可以点这里查看。务必确保团队成员已经有了最基础的重定向知识;第一步就遇到认知障碍,这事就非常令人沮丧了。
言归正传,通常来说,第一步的反向代理无须对请求做任何处理,只要简单穿透:
如果用 nginx 的话,配置也很简单,把根路劲代理到遗产系统的服务上即可:
# nginx.conf
server {
location / {
proxy_pass https://legancy.com;
...
}
}
复制代码
迁移功能
一旦 HTTP 反向代理就绪,我们就可以开始抽取功能代码了。当然,迁移新功能又有好多策略,比如分离表现层、重构数据库、提取领域服务等等;由于篇幅限制,我只讲最简单的表现层分离的手段。
以比较原始的 JSP 应用为例,通常可以将 JSP 绑定的 model 数据以 Restful API 的形式暴露出去;而 HTML template(UI)部分再以现代前端框架——如 reactJS——重新实现。重新实现的 UI 部分,就放到新的系统中实现。
记住,新技术栈对应的 CI/CD 也应在第一时间跟上;一系列 UI 测试也要在第一行代码的时候开始编写,不然几个月后你会发现新系统并不会比老系统强健多少。
重定向
如果 CI 配置得当,react 代码的修改从 Merge PR 到完成新系统模块更新——现阶段事实上只有静态文件——应该可以控制在几分钟内完成。
新系统怎么集成呢?我们需要把浏览器请求的特定资源重定向到新系统上:
新系统需要一个新的代理路劲(如/modern/
)以便与旧系统区分,代理配置上加个 location 即可:
# nginx.conf
location /modern/ {
...
proxy_pass http://modern.com/;
}
复制代码
当然,这时候新系统依旧不会起任何效果的。原因也很简单,重构初期,html 入口基本都在遗产系统里,除非代码里 hard code /modern/
相关请求,否则 UI 不会与新系统产生关联——当然这种修改是我们不愿意看到的。
怎么办呢?这里讲一个小技巧,利用 nginx 给所有的 html 注入一条指向 modern 的 js,配置大体如下所示:
# nginx.conf
location / {
proxy_pass https://legancy.com;
sub_filter '</body>' '<script type="module" src="https://juejin.cn/modern/app.react.js"></script></body>';
sub_filter_once on;
}
复制代码
只要给遗产系统的 html 注入 app.react.js,所有 UI 相关操作就可以在新系统代码内修改了;而遗产系统的 JSP 无需做任何改动。大家可以自己体会一下。
小结
OK,UI 重构的基本框架大致搭建完工了,之后就是根据特定业务逐步地迁移各个前端模块。
推荐在初始阶段可以将 react 当 jQuery 用——就是当 lib 用啦:通过 selector 找到遗产代码的特定 DOM,重构之;一个页面完工后,再修改 nginx.conf,用以劫持新页面到 modern 系统。如果进度顺利,在第一个页面完工后就可以体现出重构效果了——肉眼可见的加载速度。
前端迁移完后,就是后端服务的拆分了,我会在《绞杀者模式(二)》里进一步讲解,敬请关注!
碎言碎语
我曾参与过一个单体架构的遗产项目,积年累月堆积了几万个 bug;然而,作为开发人员平均每人每周的 bugfix 量仅为 1(时常还能引入点新 bug)。我想不出任何方式能在这个项目彻底玩完前减少一些账面的 bug 数,所以就专程前往厂里的一位领导干部那里请示。他告诉我,“重新定义 bug,那些 bug 就没了!”
听完后,豁然开朗:Bug 是绝对修不完的,所以不要再纠结每周修 1 个 bug 或是 2 个 bug 这种问题了。分一点时间出来——比如 20%的人力——迁移产品,在迁移的过程中逐步捋顺业务逻辑,当迁移完工后,遗产系统的 bug 就不再是 bug 了。