軟體系統很少一開始就是遺留程式碼。它們從明確的意圖、結構和對未來的清晰展望開始。然而,隨著時間推移,需求改變,團隊更迭,商業壓力增加。結果往往是系統雖然能運作,卻感覺不對勁。它脆弱、難以理解,且抗拒變更。這就是遺留程式碼的現實。
面對這樣的系統時,直覺可能是完全重寫。然而,重寫往往比維護更具風險。解決方案不在於放棄,而在於轉型。物件導向分析與設計(OOAD)提供了一個強大的框架,用以理解、重構並改善這些系統,同時保留其已有的價值。
本指南探討如何將物件導向原則應用於遺留程式碼庫。我們將超越理論,著眼於實際策略,以識別物件、管理依賴關係,並在原本混亂的地方引入結構。目標不是為了美觀而美化程式碼,而是為了讓明天必須與之共事的人類能輕鬆維護。

🧱 理解遺留程式碼的本質
遺留程式碼不只是舊程式碼。它是缺乏足夠自動化測試來支援變更的程式碼。它通常以早於現代設計模式的風格撰寫。在許多情況下,遺留系統是使用程序式範式建構的,其中函數與全域狀態主導了架構。
從程序式思維轉向物件導向思維,需要觀點的轉變。你不再應專注於操作的順序,而應專注於實體之間的互動。這些實體就是物件。
遺留系統的關鍵特徵
- 高耦合:組件彼此緊密依賴,使得獨立變更變得困難。
- 低內聚:類別或函數執行無關的任務,導致混淆。
- 隱藏的依賴:邏輯深埋於呼叫堆疊之中,使得追蹤資料流變得困難。
- 全域狀態:系統中共享的變數在並行操作期間會產生不可預測的行為。
- 缺乏文件:程式碼本身是唯一的真理來源,但通常已經過時。
🔍 遺留系統的物件導向分析
在重構任何一行程式碼之前,你必須分析現有的系統。物件導向分析(OOA)是定義問題領域並識別解決問題的物件的過程。在遺留系統的脈絡中,這意味著逆向工程行為,以找出藏身於程序式混亂中的邏輯物件。
步驟一:識別責任
在程式碼庫中尋找明確的責任領域。即使在程序式程式碼中,也常常存在明確的功能區域。例如,處理資料庫連接的函數與格式化報表的函數具有不同的責任。
- 識別資料結構: 資料儲存在哪裡?是分散在全域變數中,還是被分組在結構中?
- 識別行為: 對這些資料執行了哪些操作?它們是否重複?
- 依領域分組: 根據商業概念,將資料與行為分配到邏輯群組中。
步驟二:將實體對應為物件
一旦識別出責任,便將其對應到物件導向的概念。這正是舊系統與新設計之間的橋樑。
- 實體: 這些代表業務的核心概念,例如 客戶, 訂單,或產品.
- 值物件: 這些是不可變的物件,用來描述特定的屬性,例如 地址 或金額.
- 服務: 這些處理不屬於特定實體的操作,例如 通知服務.
🔒 應用封裝原則
封裝是隱藏內部狀態並要求所有互動都透過明確定義的介面進行的實務。在舊有程式碼中,全域變數以及對內部資料的公開存取很常見。這會導致難以預測的副作用。
開啟類別
舊有類別通常將每個變數都公開。為了解決此問題:
- 將欄位設為私有: 限制類別內資料成員的存取權限。
- 暴露屬性: 提供取得器和設定器,在指派前驗證資料。
- 強制不變式: 確保物件在建立和修改後始終處於有效狀態。
控制存取
並非所有資料都需要在各處可見。使用存取修飾符來控制可見性。如果方法是類別邏輯內部的,則標記為私有。如果是公開合約的一部分,則標記為公開。
| 傳統模式 | 物件導向封裝模式 | 優勢 |
|---|---|---|
| 全域變數 | 私有欄位 | 防止意外的外部修改 |
| 所有內容皆公開方法 | 基於介面的存取 | 降低模組之間的耦合度 |
| 在商業邏輯中直接存取資料庫 | 儲存庫模式 | 將邏輯與資料儲存分離 |
🧬 管理繼承與組合
繼承允許一個類別從另一個類別繼承屬性和行為。雖然實用,但傳統程式碼經常面臨深度且複雜的繼承層次結構,難以導航。這通常被稱為「脆弱基底類別問題」。
組合優於繼承
現代設計中更安全的方法是使用組合。物件不再繼承行為,而是持有提供該行為的其他物件的參考。
- 彈性行為: 您可以在執行時期透過更換組合物件來改變行為。
- 更明確的界線: 關係在類別定義中是明確的。
- 降低耦合: 基底類別的變更不會如此劇烈地傳播到整個層次結構中。
重構繼承鏈
如果您遇到長串的繼承:
- 提取超類別: 識別共通之處,並將其提取到新的基底類別中。
- 取代繼承: 將邏輯移至獨立的服務中並注入。
- 使用混入: 如果語言支援,可使用混入來實現特定行為,而無需完整的繼承。
🎭 利用多態性
多態性允許物件被視為其父類別的實例,而非其實際類別。這使得程式碼能統一處理不同類型的物件。舊有程式碼通常使用條件邏輯(if-else 或 switch 陳述式)來處理不同類型,這違反了開閉原則。
消除條件邏輯
尋找檢查物件類型的長串 switch 陳述式。這些是多態性缺失的信號。
- 建立基底類別: 為不同類型定義一個共同的介面。
- 實作特定行為: 讓每個子類別實作它所需的方法。
- 使用工廠: 建立一個根據輸入返回正確實例的物件,讓呼叫者無需知道具體類型。
介面分割
確保你的介面是具體的。一個要求每個類別都實作它不需要的方法的舊有介面,應該被拆分。這能減輕實作者的負擔,並讓程式碼更容易測試。
🏗️ 建立抽象層
抽象隱藏了複雜的實作細節,僅暴露必要的部分。在舊有系統中,商業邏輯經常與基礎設施程式碼(資料庫呼叫、檔案 I/O、網路請求)混雜在一起。
引入外觀
外觀為複雜子系統提供簡化的介面。你可以將舊有邏輯包裝在外觀中,向系統其他部分呈現乾淨的 API。
- 解耦進入點: 新程式碼與外觀互動,而非與舊有邏輯互動。
- 逐步取代: 你可以逐步取代外觀的底層實作,而不會破壞呼叫者。
依賴注入
硬編碼的依賴會讓測試與取代變得困難。引入依賴注入,讓物件能從外部接收其依賴。
- 建構子注入: 在建立物件時傳遞依賴。
- 設定器注入: 在建立後設定依賴(應謹慎使用)。
- 介面注入: 依賴定義了注入機制。
🧪 重構的測試策略
在沒有測試的情況下重構舊有程式碼是危險的。你需要一個安全網,以確保行為保持一致。
黃金主測試
當你無法輕易修改程式碼以加入測試時,將系統的輸入與輸出記錄為「黃金主檔」。以這個記錄來執行你的測試。如果輸出結果改變,你就知道有東西壞掉了。
特徵測試
撰寫描述目前行為的測試,即使該行為存在缺陷。這些測試會捕捉「現狀」。在重構過程中,這些測試能確保你不會意外修復使用者依賴的錯誤。
重構元件的單元測試
一旦你已提取出一個類別或函式,就為它撰寫單元測試。將邏輯與基礎架構分離。這樣你就能在不擔心整個系統的情況下,重構該元件的內部實作。
⚠️ 應避免的常見陷阱
重構是一個細膩的過程。有些常見錯誤可能會拖慢進度,或引入新的錯誤。
- 過度設計:不要引入不必要的設計模式。針對目前的需求,盡可能保持設計簡單。
- 忽略測試:沒有測試計畫就絕對不要重構。如果無法測試,就不要更改。
- 大爆炸式重構:不要試圖一次修復整個系統。應以小而逐步的方式進行。
- 忽略背景:理解業務領域。僅為美觀而重構,可能會讓領域專家更難理解程式碼。
📊 衡量改進成效
你如何知道你的重構是否有效?你需要能反映程式碼健康狀態與可維護性的指標。
| 指標 | 目標 | 為何重要 |
|---|---|---|
| 環複雜度 | 降低 | 表示函式中存在多少條執行路徑。數值越低,越容易測試。 |
| 程式碼覆蓋率 | 提高 | 確保更多程式碼能被測試覆蓋。 |
| 測試執行時間 | 更快 | 表示更好的隔離性與更少的相依性。 |
| 技術債務比率 | 較低 | 估算由靜態分析發現的問題的修復成本。 |
🔄 迁移的戰略方法
有時,若不造成巨大擾動,無法直接將物件導向原則應用於現有的程式碼庫。在這些情況下,戰略模式有助於彌補差距。
榕樹模式
此模式涉及逐步以新服務取代舊有功能。你會在舊系統旁邊建立一個新系統,並逐步將流量導向新系統,直到舊系統被完全移除。
外觀模式
建立一個統一的介面來包裝舊程式碼。新程式碼呼叫外觀。隨著時間推移,外觀可被新的實作取代,而舊程式碼則被遺留。
依賴注入容器
使用容器來管理物件建立與相依性。這讓您能在不更改客戶端程式碼的情況下,將舊的實作替換為新的實作。
🛡️ 風險緩解
遺留系統中的每一項變更都伴隨著風險。緩解風險需要謹慎的規劃與溝通。
- 功能開關:使用旗標來啟用新功能,而無需將其部署給所有使用者。
- 金絲雀發佈:首先將變更部署給一小部分使用者。
- 回滾計畫:確保有一種經過驗證的方法,可在問題出現時快速回滾變更。
- 溝通:讓利害關係人了解進度與潛在風險。
🧩 關於演化的最後想法
重構遺留程式碼並非一次性的專案,而是一個持續改進的過程。透過應用物件導向分析與設計原則,您能將系統從靜態負擔轉變為動態資產。
關鍵在於耐心。不要急躁。專注於小型且可驗證的改進。確保每一步都讓系統更安全、更易於理解。長久下來,這些微小的改變累積成顯著的轉變。
請記住,目標並非完美,而是進步。今天稍微改善的系統,就是對現狀的勝利。透過遵循物件導向原則,您將建立一個能應對商業需求變化的穩固基礎。











