和 Legacy Code 當好朋友


Legacy Code 應該是所有軟體工程師們心中共同的痛,幾乎可以和 wtf 畫上等號惹。 除了程式維護上的困難之外,對於工程師們的開心指數也是有著可觀的傷害力。 但是 legacy code 的問題這麼常見,也代表著它是一種很容易發生的現象。 所以在想著怎麼避免產生legacy code 的同時,學習如何與它和平共處,甚至發展出友誼(?)也是很重要的一環。

在這篇文章裡面我想分享在遇見 legacy code 的時候,如何面對它、處理它、放下它。

Legacy Code 的定義

在討論 Legacy Code 的一切之前,必須先為它下個定義。在 wiki 上面的定義是:

Legacy code is source code that relates to a no-longer supported or manufactured operating system or other computer technology.

Legacy Code 所帶來的問題:

Legacy code 的問題在於,有一部份的程式已經沒有在使用了,又或者它的設計並沒有跟上實際上 usecase 的演進。 這樣的程式對於 developer 來說,就是一種 “我不知道它在什麼時候被用到,也不知道它為什麼要這樣做” 的狀態。 因為不知道什麼時候會被用到,所以自然也不敢去改動它。也因為”不知道為什麼要這樣做”, 導致後續要新增的行為,有可能會要去滿足一些比實際需要更複雜的邏輯。並且這樣的狀況會是一個惡性循環的狀態,因為以 legacy code 為基底而發展出來的程式,通常一寫出來就立刻又成為 legacy 了。

但我認為更嚴謹並且實際的定義,則是 Working Effectively With Legacy Code 寫到的:

Legacy code is code without unit tests.

意思就是說,沒有 unit test 的 code 就是 legacy code,即使它是昨天才寫出來的也一樣。

為什麼呢? 因為其實如果程式有相對應的 unit test 的話,要去定義出 “沒有在使用的程式” 或者是去調整它的行為,讓它符合現有的 usecase 都是相對容易的 (註1)。相反地,如果程式沒有 unit test 的話,它馬上就會變成一份沒有人敢改動或者調整的 code,那它為 developer 帶來的負擔,其實和 legacy code 其實是相去不遠的。

Legacy Code 的原因

要去解決 legacy code 的問題之前,了解它為什麼會這麼容易跑出來是很重要的。因為只有在了解它的成因之後,我們才有可能從根源去避免它再度發生。 雖然每次在下 git blame 的時候從是一邊在心裡暗幹,但是 legacy code 的成因絕對不(只)是之前的同事們太沒經驗太不心這麼單純而已。

大部份的時候,legacy code 的成因來自於對於開發速度的 trade off。

在軟體的世界裡面,feature 的需求常常是千變萬化, 朝令夕改的。事實上,這樣快速演進的特性正是很多團隊成功的最重要原因之一。 但在程式的開發上,這樣快速的演進,常常造成程式的架構和實際功能的分歧。在幾次快速的演進之後,最常見的狀況就是我們會傾向於為程式預留很多彈性。

但是預留彈性的另一面,意味著增加了複雜度。而根據莫非定律,通常我們預留的彈性都不會被使用到,真正的feature演進總會朝向我們沒有想到的方向發展。

解決 legacy code 的問題是一件很難的事情。甚至連要去面對它,都不是件容易的事, 因為

沒有人想要碰爛 code

但當我們看到爛 code 卻又不得不處理的時候,心生賭爛是在所難免的。但是在一陣賭爛結束之後,更重要的是靜下心來,用正面的心態去了解問題的核心。

這個和程式其實沒有直接關係。但說到底,legacy code 的成因其實也多半和程式本身沒有直接關係。 常見的原因是產品開發的速度,沒有為 refactor 留下應有的時間,也有可能是團隊沒有足夠的經驗去設計出足夠適應產品步調的程式。 但理想上各個不同角色的人,其實目標是一致的。

簡單的說如果產品開發速度太慢公司做不起來,程式寫得再好也是沒三小路用。

找到問題的核心

接下來終於要進入如何處理 legacy code 的部份惹。 在程式的架構上,我認為很重要的第一步是 找到問題的核心階層

多半的問題都有一個最根源的痛點。

以一個 web app 為例,我們發現”這邊的 code 好像改起來很痛苦”的同時,有可能是

  • Application layer 中
    • 某一個 class 裡面的一個 method 的寫法不好
    • 各個 class,module 的切割不明確
  • Persistence (database) layer 中
    • schema 沒有 normalize 或少了相對應的 constraint,導致 query 的時候要在各個地方檢查
    • schema 的設計已經不合時宜了: 例如說 schema 上是一對多的關係,但實際上只會出現一對一的 usecase
  • Architecture layer
    • App 已經長到很複雜了,所以 single page app 的 architecture 可能比 server render 要適合
    • 在某些情況下,event stream 或 async 的 call 可能比較合適
  • Product layer
    • 可能實際上用戶使用產品的方式已經和目前的實作法式有所出入,在實作方式沒有跟著一起修改的狀況下,就會出現很多奇怪的 special case

上面只是一些可能的例子,但是可以看見的是,問題的核心有可能出現在不同的抽象層。 發生在不同抽象層的問題有可能互相影響,例如說,如果 database 的 schema 沒有設計好,那application layer 的程式就很難有漂亮的架構。 更多的時候,問題可能出現在不只一個抽象層,例如說schema 又沒有設計好,code 的計也有改進的空間。

但不管是哪一種情形,先找到問題的核心階層是解決它的第一步。

如何處理 Legacy Code

在找到問題之後,下一步就是試著去處理它惹。處理 Legacy Code 的方法其實沒有一個通用解,基本上是非常的 case by case 阿! 但是即使是這樣,還是有一些原則和方法可以參考:

跟著 Feature 演進一起改

在處理 legacy code ,或者是做任何程式架構上的調整的時候,最常出現的對話就是:

“它現在可以用嗎?”

“可以是可以,但是…” (被打斷)

“可以用的話那幹麻改?”

是的,可以用的東西為什麼要花時間去改呢?應該說以一個產品進行的角度來說,什麼事都不做就只為了改程式的架構是無法接受的。 因為改了程式的架構,對於使用者來說並沒有任何感覺阿!除此之外,在大部份的情況下,feature 是會一直演進的。 舉一個極端的例子,如果我們停下來專心用三個星期改了一部份的程式架構,很有可能再我們一改完,那個feature立刻就被砍掉了。 這樣的話花了三個星期的 refactor 就幾乎沒有任何用處。

換個角度來說,如果我們可以找到改動 feature 的時機,然後在這個時候”順便”處理掉 legacy 的問題,這樣的話就可以讓修改 legacy code 也可以跟得上產品開發的步調了。

舉例來說,假設我們有一大塊前端的功能,裡面有很多 legacy 的問題,但我們不太可能把這整個東西在UI、功能都不變的情況下重做一次。 這時候如果這塊功能的 UI 要大調整,這個時候因為不處理 legacy 的話,要大調 UI 根本是不可能,所以自然而然就可以把 legacy “順便” 處理掉了。 而在這之後,也可以讓這塊功能變得更好維護。

找投資報酬率最高的優先處理

通常 legacy code 的出現,絕對不會只有一個地方需要調整。(就像家裡如果出現蟑螂的話 可能後面有一個家庭之類的) 這時候可以先分析一下有哪些地方可以處理,然後評估各個地方處理起來的成本,選最划算的做。 處理成本基本上就是把它 refactor/重寫 需要多少人力和時間, 但怎麼才算是最”划算”的呢?也就是說,該怎麼定義處理 legacy code 的”報酬”呢?

我覺得大概可以分成幾個面向:

1. 程式的架構 (可維護性)

最常見也最可以想像的是程式的可維護性。通常 legacy code 的特色就是沒有人知道它在幹麻,也沒有人可以改得動它。 這時候每一個人、每一次有feature要跟它界接的時候,就會花上比平常更多的時間,甚至會有時程無法估計的問題出現。 解決這類型的問題,可以讓整體的開發速度變快。並且如果有一部份的 feature 是預計在之後會很頻繁的被修改的, 那解決它的報酬就是高的。反之,如果有一部份的 feature 可能不太會更動了,那即使它有 legacy issue 也可以暫時先不管它。

2. 執行的速度 (performance)

Leagcy code 常常會伴隨著 無法 finetune 的 performance issue。通常要抓到 performance 的問題點所在,在詳細的 profile 之後,接著就要仔細的去程式裡面找到出問題的點,進一步解決它。但是如果這時候程式錯綜複雜,光是要找到問題就難了,更別提要解決它。 解決這類型的問題,可以讓程式的執行速度變快,並且在之後,讓 performance 的問題更容易被發現。 但這時候可能就要看這部份實際上被使用者用到的機會高不高。也就是說,假設有一個頁面因為 legacy 的因素造成 performance 下降,那如果那一頁每天很多人都會點到,那解決起來就會比較划算。

3. 開發的速度 (build time, compile time)

這類型的問題比較常被忽略。開發的速度指的像是 “build code 的時間”、”跑 unit test 的時間” 等等。有時候一些 legacy issue 會造成這些事情的速度被拖慢,進而造成可怕的後果。例如說,如果 database schema 設計的不好,在跑 unit test 的時候,可能會讓本來可以用 transaction 來清除 testing data 的地方變成一定要用 truncation,這樣一來每個有使用到 database 的 test case 就都變慢一點點。但一份程式可能有數千個 test case,這樣一來我們把多出來的時間 x 每天跑 unit test 的次數(理論上會跑超多次才對)x team member 的數量,整個加總起來是很可觀的。更可怕的是,如果因為 unit test 跑太慢,於是讓 developer 選擇在某些時候省略它,這樣進而造成 debug 的時間變長,並且 code quality 下降而形成一個惡性循環。

在這種狀況之下,開發的速度也是一個重要的用來評估要不要解決 legacy code 的指標。意思是說,如果有一些地方的 legacy code 解決起來可以大幅加快開發速度的話,那它也會是一個很划算的選擇。

如何避免 legacy code

要怎麼樣可以不用處理 legacy code 的問題呢?最好的方法就是從一開始就避免。 但是很明顯的,這絕對不是一件容易的事情。下面是一些可能可以參考的方向:

文化

沒錯,一個 team 的文化絕對會和程式的品質有高度相關。 我認為,最根本的作法是要讓一個 team 有”在乎code的品質”的DNA。沒有人可以一直不犯錯,但前題是要 developer 自己在乎自己寫出來的程式,去做後續的維護才有意義。

在這之外,在面對和解決 legacy code 的時候,心態一定要正面。 常常看到一段寫得不好的程式,反射動作就是立刻 git blame 加 wtf。但是這樣對於整件事情其實是有負面的影響的。寫程式是一個漫長的學習過程, 不管是多厲害多有經驗的 developer,也總是會遇到不熟悉的東西,設計出不好的架構。 但絕大多數的時候,好的程式不是寫出來的,而是改出來的。 要有正面態度去面對這件事情,才可以讓整個團隊都經由錯誤去學習進而成長。

要記得我們要解決的是 legacy code 而不是 legacy people 阿!

YANGNI

YANGNI 的全名是:You ain’t gonna need it

在需求快速又大幅度的變化的狀況之下設計程式是非常困難的。我們總是想要把東西預留一些彈性以應對接下來”可能” 的變化。 但是大多數的時候,這些預留下來的彈性都沒有真的派上用場,因為實際的需求總是往不同的方向。並且當初預留下來的彈性, 都變成了不必要的複雜度存在於我們的程式裡面。

一個比較好的做法是,除了明確的需求之外,不多預留任何彈性。直到需求出現的時候再做上去。

這樣的方法看起來好像很偷懶又慢,但是其實它真的可以幫我們省下很多改來改去的時間。 而且在沒有多餘複雜度的狀況之下,要修改程式也會簡單的多。 我認為這樣的想法的出發點就是 沒有用到的東西不要出現在 code 裡面

這同時也包含了,如果有某一個功能確定被拿掉了,那就把那邊的程式也移除掉吧。 (不要擔心以後想念它的時候會找不到,git 會幫你記住的。) 另外,當程式的 business logic 改變的時候,要儘快把程式的架構邏輯也立刻調整到符合現有的狀況, 而不是用一些 special case 來處理,這也是很重要又經常被忽略的點之一。

測試

不得不說,自動化測試真的是一切的基礎阿。 在上面提到的每一個動作,其實都牽涉到要 refactor 現有的程式。 這時候如果沒有完整的 unit test 保護我們,簡直就是寸步難行。

並且當我們在寫測試的時候,某種程度會迫使我們去重新思考程式的架構, 通常這樣的程式比較不會歪到太誇張。

結論

Legacy code 應該是所有 developer 都會遇到的問題。我覺得在面對、解決它的時候,會從中學到很多東西。 最重要的部份可能還是在溝通,畢竟你要跟另一個人說 “嘿!我覺得你寫的 code 臭臭的耶!” 本來就很不容易吧。 但這樣的溝通要站在一個正面的角度下進行, 因為可能當初這個人在設計程式的時候,有什麼我們不了解的特殊情境, 更有可能過了一陣子之後,寫出 legacy code 的人輪到了我們自己。 在這個(一直罵髒話的)過程中,要一直提醒自己,這一切都是透過錯誤來學習的珍貴機會。

再來是解決 legacy code 的原因,並不是單純為了看它不爽或為了美感而解決的。 大部份的時候,需要有一個夠充份的實際原因我們才會做這件事。 在這當中,去評估各種可能性的 trade off 也是一個很重要的部份, 畢竟身為工程師,在各種時候做最精準的 trade off 是我們最重要的職責之一吧。

註1

話雖如此,但前題其實是要有 寫得好的 unit test…