重構或重寫 Legacy code 的幾個階段

看完前一篇的介紹,我想你應該已經想好面對 legacy code 時,應該要重構還是重寫了。如果你打算對正在線上的程式碼進行重構,那就像在幫飛行中的飛機換引擎一樣;如果是重寫,那就是造一架新飛機讓它升空,然後在空中把舊飛機裡的乘客接過來一樣。

那麼要怎麼在空中幫飛機換引擎或接送乘客呢?應該有些具體的方案吧?其實有幾件事是兩個方法都必須先做的,以下就整理了幾個重構或重寫前重要的步驟。

綜覽全局

不論你打算重構或重寫,我都會建議你花點時間整理一下專案目前的規格以及各功能的使用情境;這些資訊的來源可能有好幾種:文件裡或 issue tracking 系統中有記載的、專案關係人或曾經維護過該專案的同事還記得的、直接從 UI 上操作後所得到的結果等等。

這些資訊可以用白板加便利貼來整理,或是使用 Trello 讓大家都能看到;如果專案太過龐大,那麼至少整理一下你負責的部份。這麼做的原因是幫助你或團隊更為理解這個專案的全貌。同時也可以趁這個時候去確認功能的存廢,以及是否有哪些規格與實作的對應上其實有問題的。

你可能會覺得這應該是 PM 的工作,但我必須說,現實中的你就是 PM 的延伸。當然如果是整個團隊決定要重構或重寫的話,就讓大家一起來做這些工作吧。

理解 legacy code 的架構

如果你不知道 legacy code 是怎麼運作的,那就別想要重構或重寫了。有時候 legacy code 中的某些設計,通常是因為當時的時空背景而產生的。所以當你能夠從系統架構到程式架構來深入理解前人在 legacy code 中做了哪些事情,就能瞭解你面臨了什麼樣的限制。

在理解的過程中,你可以嘗試整理出一些開發文件,釐清程式需要的系統環境、使用的資料庫或外部服務、或是相依的套件等資訊。得到這些資訊後,你可以著手分析應該哪些部份應該重構或是重寫;因為有時候可能會因為某些 legacy code 而導致系統無法進行安全性升級,或是服務轉換上的困難,像這類的程式碼就可能要進行重寫。

導入持續整合與自動化佈署

多數 legacy 專案從開發到上線的過程中,常常有很多地方需要人工介入,像是靜態分析、程式碼風格及語法檢查、測試、佈署等等。透過導入持續整合 (Continuous Integration 簡稱 CI) 以及自動化佈署後,這些專案就再也不是 legacy 專案了;整個建置佈署的過程完全自動化,因為人工介入而出錯的機會大大降低。

這一步看起來對實際的程式碼沒什麼影響,但事實上對開發者心理層面影響非常巨大。一般來說,這個流程還會搭配版本控制系統的分支來做不同執行環境的自動化佈署,分離出測試環境與正式環境;在程式佈署到測試環境時,就能讓開發者更快速地發現某些無法透過單元測試來找出的整合性問題。

導入 e2e 自動化測試

另一項更重要的工作是自動化測試,它對程式來說就像是個安全保險。所謂的 legacy code 通常就是指沒有自動化測試的程式碼,所以每次在修改它們時總是會讓人心驚膽跳,深怕改東壞西。因此不論是想重構或是重寫,都必須先為程式碼加自動化測試才行。

但這時候幫細部程式做單元測試其實不是很明智的抉擇,因為大粒度的重構或是整個重寫,都會讓程式單元有大幅度的調整。因此通常會依據正在上線的程式所呈現的外在行為來做為測試結果的驗證基準,一般來說這就是指撰寫 e2e 測試;而前面整理出來的規格與情境,這時就可以用來 e2e 測試的測試案例了。

完成 e2e 測試後,接下來就可以進行重構或重寫了。而重構或重寫都有某些技巧,以下介紹幾個:

選個好 IDE 讓自己看見 legacy code 的病因

以 PHP 為例,很多人會覺得用 VIM 或其他程式編輯器來開發 PHP 就已經足夠了,當然這沒有什麼不好。只是當每次我用 PhpStorm 打開用他們寫的程式碼時,就會看到紅色波浪底線、土黃色背景的常數、灰色的變數或類別,心中就會有種不快的感覺。因為你太晚知道你的程式碼有病,而這些病是其他編輯器沒有幫你找到的。

也許你想到了程式碼靜態分析是可以發覺這些問題的方法,只是大多數開發者不會意識到要去做這件事;而且即便知道該做而去做,時間一久也會覺得麻煩或根本就忘了這回事。也許有些人想說乾脆在 commit 前先讓工具去自動檢查吧,這聽起來像是個好主意;只是要等到每次 commit 才會發現這些問題,你早就遺失當下 coding 時的 mind context 了。

用功能切換 (Feature Toggle) 來重構

面對絕大部份的 legacy code 我們很難一步到位地重構它,這時我們可以透過 feature toggle 這種手法來將系統的功能一個一個地轉換到新的架構上。 Feature toggle 可以讓新舊程式架構同時並行,利用開關變數來切換新舊程式。

由於系統對外的 API 或頁面並沒有改變,而且在 e2e 測試的保證下,使用者其實並不會有所感覺。另一個好處就是,即使是在重構的過程中有新需求的加入,就可以將它放到新架構中,不必回到 legacy code 中再做一次。

用框架重新改寫

如果 legacy code 連 e2e 測試都不好做,那麼表示它跟環境的關係已經根深蒂固了。我在遇到這類專案時,選擇的就是重寫。只不過重寫不能再重蹈覆轍,所以選擇一個能夠方便做 e2e 測試或整合測試的框架就是我重寫的第一個目標。雖然說 e2e 測試或整合測試也是可以自己來,但與其把力氣花在測試架構的設計上,不如去找個優秀框架來得快;因為優秀的框架通常已經把很多底層的工作抽象化,搭配整理好的規格與情境所撰寫的 e2e 測試,就可以讓開發者專注在功能面的開發上。

在用框架重寫時,可以把握一個重點:越高層越接近需求,越底層越接近實作;也就是利用框架的抽象機制來封裝底層的操作細節,例如資料庫或快取等。這麼一來即使底層服務需要抽換,也不至於影響到高層的邏輯。很多 legacy code 就是在邏輯層還夾雜很多底層服務的操作,結果在系統需要更換環境時,得花很多力氣在修改這些程式碼。

討厭寫文件?那就改用 BDD (Behavior-Driven Development)

重寫的專案我會要求用 BDD 來開發,原因無它:因為寫好的文件就可以拿來驗證程式碼。如果你覺得寫文件很浪費時間,不如寫程式一次搞定,那麼 BDD 絕對會是你應該試試的開發模式。因為常見的 BDD 框架通常是採用 Gherkin 語法來描寫功能 (feature) ,這使得文件本身很好讀,又容易轉換成驗證用的 context 程式,所以也很適合用在 PM 與開發者合作;也就是 PM 寫 feature ,讓開發者用 feature 來驗證自己寫的程式碼。

BDD 通常是以情境來當驗證案例,所以前面收集到的規格和情境很適合用在這裡,也因此 BDD 通常會結合 e2e 測試來進行。這麼一來文件的更新,也會影響到測試是否能夠通過,就不會再發生文件和程式碼不一致的情況了。

持續保持正確的心態

不知道是哪一國的童軍守則提到:「讓出去的營地比進去時乾淨。」這是身為優秀開發者也必須遵守的好守則。面對 legacy code 我們當然可以抱怨,但也不可以因此就撒手不管。既然接手維護了,在沒什麼外力的狀況下 (通常是政治因素) ,就應該利用各種方法一步一步讓它變的更好維護,而不是成為之後接手的人的夢魘。

不論是重構或是重寫,能生出易於維護的優良程式碼絕對是開發者的驕傲。