第 1 章說明技術債,並指出「對設計壞味道與重構缺乏認知」是技術債的主要成因之一。本章正面回答兩個基礎問題:為什麼要關心壞味道?壞味道從哪裡來?

本書的學習路徑:以壞味道反推設計原則#

心理學研究指出,我們從錯誤中學到的,往往比從成功中學到的還多——關鍵在於發現自己錯了的那種「驚訝感」。

本書因此採取與傳統教學相反的路徑:

  • 傳統做法:先講設計原則,再用例子套用。
  • 本書做法:從壞味道的真實案例出發,討論它為何發生、如何處理,藉此反推被違反的設計原則。

這種「以壞味道為學習錨點」的方式,更貼近實務工程師遇到的場景,也更容易讓設計原則被內化。

2.1 為什麼要關心壞味道?#

技術債的關鍵指標就是軟體品質低落。在帶債的專案裡,常見以下症狀:

  • 軟體變得難以理解(理解性 understandability 降低)。
  • 同樣的修改與功能擴充花費的時間越來越長(可變更性 changeability可擴充性 extensibility 降低)。
  • 看似可重用的部分,實際上越來越難重用(可重用性 reusability 降低)。
  • 客戶越來越不滿意,QA 越來越難寫測試(可靠性 reliability可測試性 testability 降低)。

由於設計對軟體品質有決定性影響,本書關注以下六個品質屬性。下表中的「設計片段(design fragment)」可指類別、介面、抽象類別,或它們之間的關聯(如繼承階層、相依圖)。

品質屬性定義
理解性(understandability)設計片段被理解的容易度
可變更性(changeability)修改既有功能時,可以不引發連鎖效應地調整設計片段的容易度
可擴充性(extensibility)為支援新功能而擴充設計片段、且不引發連鎖效應的容易度
可重用性(reusability)將設計片段用於原始設計脈絡之外的問題的容易度
可測試性(testability)設計片段透過測試被偵錯的容易度
可靠性(reliability)設計片段確保功能正確、防止執行期問題的能力

範例:Multifaceted Abstraction 對品質屬性的衝擊#

java.util.Calendar 這類同時負責「日期」與「時間」的類別為例(即 3.4 節討論的 Multifaceted Abstraction),它違反單一職責原則(Single Responsibility Principle)

  • 理解性:多重責任增加認知負擔;理解每個責任、釐清彼此關係耗時費力。
  • 可變更性與可擴充性:難以判斷該動哪些成員;改動可能波及不相關的責任,造成設計層面的連鎖反應。
  • 可重用性:即使只需重用一個責任,也得連帶搬走其他不相干的責任;責任彼此糾纏時,行為變得難以預測。
  • 可測試性:責任之間緊耦合,難以分離測試。
  • 可靠性:操作不同變數的責任放在一起,容易誤存取錯誤變數,導致執行期錯誤。

壞味道的衝擊不僅止於品質屬性。對外暴露 API 的框架/函式庫一旦帶有設計壞味道,修正成本極高——客戶端已經依賴後,幾乎沒有回頭路。這直接影響組織的對外聲譽。

2.2 壞味道從哪裡來?#

設計壞味道的成因與技術債高度重疊。除了第 1 章已提及的「缺乏好的設計者」與「缺乏重構」之外,本節聚焦其他六大成因。

Figure 2.1: 設計壞味道的常見成因

違反設計原則#

設計原則本就是用來引導出高品質設計的指南,違反它們便會以壞味道的形式呈現。

  • 範例 1:java.util.Calendar 同時處理日期與時間 → 違反抽象原則(單一職責)→ 即 Multifaceted Abstraction(3.4 節)。
  • 範例 2:java.util.Stack 繼承 java.util.Vector,但 Stack 與 Vector 在概念上沒有 IS-A 關係(Stack 不能取代 Vector 使用)→ 違反階層原則(替換性 substitutability)→ 即 Broken Hierarchy(6.8 節)。

不當套用設計樣式#

設計樣式(design pattern)必須經過深思才能套用:

  • 樣式的類別圖/序列圖只是參考解,每種樣式都有上百種變體,每種變體都有自己的代價。
  • 不理解內在權衡的硬套,會產出類別過多、責任稀薄、彼此緊耦合等症狀。

壞味道與設計樣式存在有趣的雙向關係:套用適當的樣式經常是修正壞味道的最佳手段;但錯用、亂用、過度使用樣式同樣會產生壞味道(即所謂的反樣式 antipattern)。

程式語言的限制#

  • Java 早期不支援列舉,迫使開發者用類別或介面承載常數,導致 Unnecessary Abstraction(3.5 節)。
  • JDK 中的 AbstractQueuedSynchronizerAbstractQueuedLongSynchronizer 都繼承自 AbstractOwnableSynchronizer,差別僅在處理 intlong 兩種原始型別。Java 泛型不支援原始型別 → 無法消除程式碼重複 → 形成 Unfactored Hierarchy(6.3 節)。

OO 語境下的「程序式思維」#

由程序式背景轉入物件導向的開發者,常把類別當作「在做什麼」(doing things)而非「是什麼」(being things)。這種思維會以下列方式出現:

  • 以動詞命名類別。
  • 對類別進行功能性分解。
  • 用顯式型別檢查取代多型。

對應到 3.2 節的 Imperative Abstraction

黏滯性(viscosity)#

當「正確做法」比「捷徑」更費力,開發者就會傾向用捷徑——這稱為黏滯性。書中區分兩類:

  • 軟體黏滯性(software viscosity):套用正確設計時,遭遇的阻力(時間、心力、複雜度)。
  • 環境黏滯性(environment viscosity):開發環境本身對好實踐造成的阻力,例如緩慢的開發流程、低效率的工具、組織程序、法規限制等。

黏滯性高的環境會默默地把所有人推向省事的壞路徑。改善工具與流程,往往是改善設計品質最被低估的槓桿。

不遵守最佳實踐與流程#

當組織未確實執行流程或最佳實踐時,常見副作用即為設計壞味道。例如:

  • 複合重構(composite refactoring) 必須原子地整體完成;若中途停手,就會留下半成品狀態的壞味道(見 6.9 節的軼事範例)。

2.3 如何處理壞味道?#

處理壞味道的主要手段就是重構(refactoring)

整體脈絡:

  • 第 3–6 章:每個壞味道描述都附上「對應的重構建議」。
  • 第 8 章:提供透過重構系統性償還技術債的整體流程。