绞杀者模式 (一)

背景

随着系统老化、开发工具逐渐落伍、bug 堆积,项目会变得及难维护。所以“腐烂”是所有遗产项目不可避免的一环。一般企业基本不会再去碰遗产项目,但是现代很多公司却喜欢另辟蹊径——每两三年用新技术重构一遍代码。众所周知,遗产代码很难改动,那它们这么做的自信来自何处?

故事要从一个叫 Martin Fowler 的老头说起。有一次,他在热带雨林里旅游,无意间发现了一种叫“绞杀者藤蔓”的植物:

绞杀者藤蔓

绞杀者藤蔓会沿着其他树干一路向上生长,以获取光线资源;随着藤蔓的不断生长,之前的寄生树会被这种植物整个包裹,并随着阳光资源的枯竭而死亡;最后树木腐烂而逝,但是原地却留下了一颗树状的巨大藤蔓。

绞杀过程

Martin 老头回去后就把这个故事写在了博客上,并提出了一个叫“绞杀者模式”的策略:通过逐步重构单体应用(而不是推到重来的方式),逐渐构建出一个新的应用程序。这也成为了后来开发人员对遗产系统进行现代化改造的基本方针。

绞杀者模式

讲完植物,我们从实现策略上探讨一下“如何绞杀”;绞杀的大体操作如下:

  1. 创建一个门面拦截后端遗产系统的请求
  2. 通过一定的规则,门面会将特定请求分别路由到新旧系统上——反向代理
  3. 保留遗产系统的原有功能,同时在新系统中重写旧的模块,并逐渐地将请求倾斜到新系统中;迁移过程中,由于门面的隔绝,消费者照常使用现有功能,并不会有任何后台重构的感知
  4. 完成迁移后,所有请求将路由到新系统上,遗产系统被“绞死”
绞杀者模式

绞杀者的好处是:保留了遗产系统的代码,以平滑迁移的形式朝着新应用迈进;这确保了每一步前进都有回退的机会。渐进迁移的时间可能会很长,甚至可以永久性的保持部分遗产系统的功能。

实操

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 代码保持不动
Split

题外话,利用新技术栈实现功能模块后,对应的 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 重构后的页面请求顺序:

  1. 浏览器提请页面
  2. 由于 nginx 默认设置,请求都会路由到遗产系统上
  3. JSP 生成 html 后返回页面
  4. nginx 会给所有 html 返回注入脚本标签<script type="module" src="/modern/app.react.js"></script>
  5. 浏览器解析到上述标签后,再发起/modern/关联的 js 的请求
  6. 这个请求会被路由到新系统上
  7. 新系统返回 app.react.js
  8. 浏览器执行上述 js,发现有 Rest API 的请求,于是再发起数据请求
  9. 数据请求也是走默认配置路线,被 nginx 指向遗产系统
  10. 遗产系统返回 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 了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容