所謂的 Legacy Code ,一般稱為「既有的程式碼」,但或可稱為「陳年老碼」也頗符合情況,因為它的意義不單只「既有」那麼簡單,通常它還意謂著現行的開發者,認定它們為不符現今使用標準的程式碼。不符合現今使用的標準可能有好幾種,像是沒有人看得懂、無人可維護修改、不利擴充、甚至是執行效率太差等等。

很多時候,Legacy Code 代表一個長期疊床架屋的結果,不僅有著許多過時的想法和技術,同時也注滿了各種在過去當下受限於時間、受限於資源而做的妥協。所以說,Legacy Code 一般來說是不受程式設計者歡迎,但是它們之所以會存活下來,必然有它的道理。Legacy Code 最大的價值在於,它是可以正常運作的,它或許有著諸般的缺點,但是,它擁有一個很有正面意義的特質,就是它是能正確運作無誤的,它通過了現實應用的考驗,而且真的在商業上帶來價值。這使得你很難輕易加以離棄。

你也許厭惡它但不能沒有它
開發一段程式碼,實際上花在程式設計及撰寫的時間,其實佔整體開發週期的比例並不高。我們花了更長一段的時間來找出所寫下程式碼的問題,並且加以修正、使其穩定,最終可以正常的運行,產生商業上的價值。

因此,Legacy Code 再怎麼不討人喜歡,它還是擁有一個超級重要的優點──穩定。這使得許多人不捨得將 Legacy Code 更換掉,也就助長了繼續的疊床架屋,同時使得 Legacy Code 的情況愈加惡化。接下來要改變的東西更多,也就更難加以改變。

Legacy Code 有時候就像是個技術債的累積產物,如果你不試著償還一點,那麼便會連本帶利愈滾愈多,所以你可能會在一個時間點上選擇好好面對它,畢竟「出來跑的,遲早要還」。

相依性令你動輒得咎,修改不易
為什麼人們懼怕修改 Legacy Code?常常不是因為人們不願意投入心力,而是因為這些 Legacy Code 和許多環節之間都有關聯性。

一來你可能對 Legacy Code 不夠了解,所以對於它和其他程式碼之間的相依性,並不是太有把握,二來是許多陳年的 Legacy Code ,本身在設計時就沒有考慮到降低相依性的問題,彼此之間盤根錯節,可以說是牽一髮動全身,而程式設計最難的問題之一,就是修改程式碼時所造成的副作用。

當然也會有人想要打掉重練,但是除非重寫整個系統,否則系統裡的程式碼有新有舊,兩者交雜的情況下,你常常不會想要換掉全部,而只想換掉部份,在這種情況下,仍然必須面對相依性的問題,此外,還得付出讓新程式碼穩定的漫長時間代價。

在更替 Legacy Code 時,處理相依性是免不了的課題,只是處理多或處理少的問題而已。在做決定的時候,往往是考量讓程式碼穩定的時間,以及處理相依性的力氣。如果將「陳年老碼」更換為新程式碼的範圍大,那麼要處理的相依性問題就可能比較小。相反的,有可能要處理的相依性問題,可能就比較大。

雖說程式設計有好有壞,但是大抵上現代程式碼還是呈現「高聚合、低耦合」的特性。也就是說,如果設計上還是有著一定模組化的概念,那麼在同一個模組裡的相依性會高,但不同模組間的相依性會低。當一次砍掉一個完整的模組時,所涉及的相依性關係,時常會比只砍掉模組中的部份來得少。

但是,一次砍掉一個模組的風險有時比較高,因為「砍掉重練」的範圍大,意謂著程式碼需要測試、還有使其穩定的時間就拉的更長。每次的步伐距離較大,意謂著不可預期的風險就比較高。因此,這仍然是個需要視情況取捨的問題,很多時候魚與熊掌不可兼得──就如同軟體設計時大多數的問題一樣。

當你決定要「重練」的範圍之後,剩下的關鍵問題就是如何處理相依性了。有人曾經說過「什麼是 Legacy Code 呢?沒有測試案例的程式碼就是 Legacy Code」。這當然是一種觀點,是基於日後的繼續發展,也是基於修改時避免造成副作用的原因。把「陳年老碼」換新的過程中,之所以要更重視測試,便是因為對現有程式碼所做的修改可能引發的副作用。

面對更新之後的副作用

要處理好副作用的首要之務,就是處理好相依性。相依性低,副作用可能就會比較少,但若是相依性高,可能產生的副作用,就會不斷讓你驚而不喜。那麼,在換新的過程中,應當怎麼處理副作用呢?我想可以採取先整理介面的方式來進行。

介面是一個模組和外界接觸的所在,理論上,設計上要有介面。但是陳年老碼可能會有,可能不會有。當Legacy Code 的某個概念上的模組,沒有一個確切的介面時,你的首要之務應該是先整理出一套介面來,這套介面應當描述了這個模組和外界接觸的長相,通常它可能是一些類別,而每個類別中有一些函式,都是模組之外的程式碼唯一可以碰觸到這個模組的。

如果你想要重練的模組沒有介面,那麼你應該在了解模組究竟提供模組之外的程式碼什麼「服務」,將這些「服務」的方式設計成一個介面。介面本身是抽象的、沒有具體實作的,所以你只需要先設計,讓介面足以滿足模組之外的程式碼(或稱客戶端程式碼)的所有使用案例。當然,你還可以進一步考量到日後這個模組的擴充可能,將這些需求一併含括進來。

當你確定所設計的介面滿足現有、未來的可能需求之後,你就需要試著實作這組介面。我建議第一個階段是「轉接器(adapter)」階段,也就是主體仍是利用Legacy Code來完成這組介面的實作。因為這些程式碼不見得可以直接得到介面所設計的輸入及輸出,因此,須透過一些轉換才能辦到。

此時,「轉接器」的介面實作就是要負責這件事,它處理介面設計的輸入,轉換成為 Legacy Code 可以接受的輸入,再將 Legacy Code 所產生的輸出,轉換成為介面設計的輸出。

當你完成這樣的實作之後,就可以試著修改客戶端程式碼,使得它們一律轉換到「轉接器」版本的介面實作去,而此刻 Legacy code 其實還算真的開始換新。

這麼做的最大意義,是先驗證你的介面設計的確滿足客戶端程式碼的需求,而且你可以先讓此時的程式碼先開始真正的運作。由於變動的幅度小,絕大多數的邏輯仍是由 Legacy Code 所完成,因此,造成錯誤的機會低。而這麼做,你是先將客戶端程式碼「換新」,讓它們先「升級」到新的介面上去,即使還沒升級到新的實作上。

在確定新的介面運作順暢之後,接下來便可以將「轉接器」背後的 Legacy Code 實作汰換掉。你可以選擇全面汰換,或是逐步汰換。

「轉接器」的作用,在這邊就可以突顯出來了,因為它不但是一個可以運作的實體,而且它還允許你漸進地把背後的實作,慢慢換成新版的實作。所以你的選擇就變多了,你可以綜合考量時間、資源等因素,來決定你的換新計畫,但你更新的步調如何,你總是在從一個百分之百的「轉接器」實作版本換新,成為一個全新版本實作的道路上,不斷前進,而且每踏出換新的一步,你就得到一個變得更新,而且仍舊可以運作的版本。最終,你可以把這介面背後的 Legacy Code 實作,都換為想要的實作。

如此一來,處理Legacy Code的步驟就是:(1)決定更新的邊界(2)依據邊界範圍設計介面(3)實作新介面的轉接器實作(4)把客戶端程式碼升級到新的介面版本(5)依計畫將轉接器實作更新為全新實作。

這提供了演化的可能,也是我最喜歡的軟體開發方式,它降低風險,而且在短期內可驗證效果,也鼓舞每個人有信心繼續做下去。

專欄作者

熱門新聞

Advertisement