背景
隨著系統老化、開發工具逐漸落伍、bug 堆積,項目會變得及難維護。所以“腐爛”是所有遺產項目不可避免的一環。一般企業基本不會再去碰遺產項目,但是現代很多公司卻喜歡另辟蹊徑——每兩三年用新技術重構一遍代碼。眾所周知,遺產代碼很難改動,那它們這么做的自信來自何處?
故事要從一個叫 Martin Fowler 的老頭說起。有一次,他在熱帶雨林里旅游,無意間發現了一種叫“絞殺者藤蔓”的植物:
絞殺者藤蔓會沿著其他樹干一路向上生長,以獲取光線資源;隨著藤蔓的不斷生長,之前的寄生樹會被這種植物整個包裹,并隨著陽光資源的枯竭而死亡;最后樹木腐爛而逝,但是原地卻留下了一顆樹狀的巨大藤蔓。
Martin 老頭回去后就把這個故事寫在了博客上,并提出了一個叫“絞殺者模式”的策略:通過逐步重構單體應用(而不是推到重來的方式),逐漸構建出一個新的應用程序。這也成為了后來開發人員對遺產系統進行現代化改造的基本方針。
絞殺者模式
講完植物,我們從實現策略上探討一下“如何絞殺”;絞殺的大體操作如下:
- 創建一個門面攔截后端遺產系統的請求
- 通過一定的規則,門面會將特定請求分別路由到新舊系統上——反向代理
- 保留遺產系統的原有功能,同時在新系統中重寫舊的模塊,并逐漸地將請求傾斜到新系統中;遷移過程中,由于門面的隔絕,消費者照常使用現有功能,并不會有任何后臺重構的感知
- 完成遷移后,所有請求將路由到新系統上,遺產系統被“絞死”
絞殺者的好處是:保留了遺產系統的代碼,以平滑遷移的形式朝著新應用邁進;這確保了每一步前進都有回退的機會。漸進遷移的時間可能會很長,甚至可以永久性的保持部分遺產系統的功能。
實操
OK,理論很簡單,實操中會有各種各樣的問題,重構過程通常要持續數月甚至數年。僅僅宣稱“通過絞殺者模式可以實現系統遷移”,這種話是沒用的:一想到你要動所有代碼,大家立馬就慌了,決策者(不管懂不懂技術)都不會如此草率地行動。所以最好能有更細節的拆分步驟。所謂的“拆分”,可以從表現層、服務層、持久層等等方面入手;只要記住從最簡單的部分開始,當顯現出價值后,就可以持續加速改造了。
通常來說,表現層的拆分是最簡單的,比如將 JSP 或 tymeleaf 這種后端渲染框架拆成 restful api + react 的形式——動靜分離——就可以很快出貨。我們看看具體的改造過程。
反向代理
實現絞殺者模式的第一步自然是加個門面啦。我能想到最最最簡單的門面就是:專職的反向代理工具 nginx 了。反向代理我之前寫過一篇文章,大家可以點這里查看。務必確保團隊成員已經有了最基礎的重定向知識;第一步就遇到認知障礙,這事就非常令人沮喪了。
言歸正傳,通常來說,第一步的反向代理無須對請求做任何處理,只要簡單穿透:
如果使用 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 到完成新系統模塊更新——現階段事實上只有靜態文件——應該可以控制在幾分鐘內完成。
新系統怎么集成呢?我們需要把瀏覽器請求的特定資源重定向到新系統上:
新系統需要一個新的代理路勁(如/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 了。