背景
随着系统老化、开发工具逐渐落伍、bug 堆积,项目会变得及难维护。所以“腐烂”是所有遗产项目不可避免的一环。一般企业基本不会再去碰遗产项目,但是现代很多公司却喜欢另辟蹊径——每两三年用新技术重构一遍代码。众所周知,遗产代码很难改动,那它们这么做的自信来自何处?
故事要从一个叫 Martin Fowler 的老头说起。有一次,他在热带雨林里旅游,无意间发现了一种叫“绞杀者藤蔓”的植物:
绞杀者藤蔓绞杀者藤蔓会沿着其他树干一路向上生长,以获取光线资源;随着藤蔓的不断生长,之前的寄生树会被这种植物整个包裹,并随着阳光资源的枯竭而死亡;最后树木腐烂而逝,但是原地却留下了一颗树状的巨大藤蔓。
绞杀过程Martin 老头回去后就把这个故事写在了博客上,并提出了一个叫“绞杀者模式”的策略:通过逐步重构单体应用(而不是推到重来的方式),逐渐构建出一个新的应用程序。这也成为了后来开发人员对遗产系统进行现代化改造的基本方针。
绞杀者模式
讲完植物,我们从实现策略上探讨一下“如何绞杀”;绞杀的大体操作如下:
- 创建一个门面拦截后端遗产系统的请求
- 通过一定的规则,门面会将特定请求分别路由到新旧系统上——反向代理
- 保留遗产系统的原有功能,同时在新系统中重写旧的模块,并逐渐地将请求倾斜到新系统中;迁移过程中,由于门面的隔绝,消费者照常使用现有功能,并不会有任何后台重构的感知
- 完成迁移后,所有请求将路由到新系统上,遗产系统被“绞死”
绞杀者的好处是:保留了遗产系统的代码,以平滑迁移的形式朝着新应用迈进;这确保了每一步前进都有回退的机会。渐进迁移的时间可能会很长,甚至可以永久性的保持部分遗产系统的功能。
实操
OK,理论很简单,实操中会有各种各样的问题,重构过程通常要持续数月甚至数年。仅仅宣称“通过绞杀者模式可以实现系统迁移”,这种话是没用的:一想到你要动所有代码,大家立马就慌了,决策者(不管懂不懂技术)都不会如此草率地行动。所以最好能有更细节的拆分步骤。所谓的“拆分”,可以从表现层、服务层、持久层等等方面入手;只要记住从最简单的部分开始,当显现出价值后,就可以持续加速改造了。
通常来说,表现层的拆分是最简单的,比如将 JSP 或 tymeleaf 这种后端渲染框架拆成 restful api + react 的形式——动静分离——就可以很快出货。我们看看具体的改造过程。
反向代理
实现绞杀者模式的第一步自然是加个门面啦。我能想到最最最简单的门面就是:专职的反向代理工具 nginx 了。反向代理我之前写过一篇文章,大家可以点这里查看。务必确保团队成员已经有了最基础的重定向知识;第一步就遇到认知障碍,这事就非常令人沮丧了。
言归正传,通常来说,第一步的反向代理无须对请求做任何处理,只要简单穿透:
Reverse Proxy如果使用 nginx 的话,配置也很简单,把根路劲代理到遗产系统的服务上即可:
# nginx.conf
server {
location / {
proxy_pass https://legacy.com;
...
}
}
迁移功能
一旦 HTTP 反向代理就绪,我们就可以开始抽取功能代码了。当然,迁移方式上又有好多策略,比如分离表现层、重构数据库、提取领域服务等等;由于篇幅限制,本期只讲最简单的表现层分离。
以比较原始的 JSP 应用为例,通常可以将 JSP 做“动静分离”的重构:
- 动态部分:即原先绑定在 JSP 上的 model 数据。将它们以 Rest API 的形式暴露出去,JSP 以下的业务逻辑保持不动
- 静态部分:即 JSP 的 HTML 模版(UI)部分。这部分以现代前端框架——如 reactJS——重新实现。重写的 UI 代码全部放到新的系统中,遗产系统中的 JSP 代码保持不动
题外话,利用新技术栈实现功能模块后,对应的 CI/CD 也应在第一时间跟上;一系列 UI 测试也要在第一行代码起开始编写。不然几个月后,你会发现新系统并不会比老系统强健多少。
重定向
如果 CI 配置得当,react 代码的修改从 Merge PR 到完成新系统模块更新——现阶段事实上只有静态文件——应该可以控制在几分钟内完成。
新系统怎么集成呢?我们需要把浏览器请求的特定资源重定向到新系统上:
Redirect新系统需要一个新的代理路劲(如/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://legacy.com;
sub_filter '</body>' '<script type="module" src="/modern/app.react.js"></script></body>';
sub_filter_once on;
}
只要给遗产系统的 html 注入 app.react.js,所有 UI 相关操作就可以在新系统代码内修改了;而遗产系统的 JSP 无需做任何改动。
数据绑定
上面提到了“动静分离”,那数据和模版分离后,两者怎么互相绑定呢?可能有些朋友会有疑惑,这里补充说明一下,react.js 通常利用异步请求 Rest API 的形式获取数据,并在它自己的模版上实现绑定。这是所谓的MVVM 模式,也是现代化前端框架优于 JSP 框架的重要原因。
我们看看加了 react 重构后的页面请求顺序:
- 浏览器提请页面
- 由于 nginx 默认设置,请求都会路由到遗产系统上
- JSP 生成 html 后返回页面
- nginx 会给所有 html 返回注入脚本标签
<script type="module" src="/modern/app.react.js"></script>
- 浏览器解析到上述标签后,再发起
/modern/
关联的 js 的请求 - 这个请求会被路由到新系统上
- 新系统返回
app.react.js
- 浏览器执行上述 js,发现有 Rest API 的请求,于是再发起数据请求
- 数据请求也是走默认配置路线,被 nginx 指向遗产系统
- 遗产系统返回 Rest API 的 JSON 数据
最后,就是在浏览器上 react 框架自动实现数据绑定了。相较于 JSP 那种一股脑后端渲染并返回一个巨大的 HTML,“动静分离”的模式在过程中相对复杂一点,新入门的朋友可能要增加点认知成本了。但最终体现在代码上的话,其实更简单;这东西写过一两个页面就能感觉到了,我这里不深入讲解了。
小结
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 了。
网友评论