《Design Patterns》一書開宗明義:「多用組合,少用繼承」。繼承雖能復用代碼,卻造成強耦合、破壞封裝、且難以應對「單一方法復用」需求。本文深度解析三種實戰策略,幫助你寫出高內聚、低耦合、易測試的代碼。
痛點: 領域物件 `Polygon` 混雜了資料與演算法 (`getPerimeter`, `isConvex`),子類若只想復用 `containsPoint` 卻被迫繼承整個類別。
解法: 將演算法提煉為無狀態的 `static` 方法,放入專門的工具類 (命名慣例可用 `PolygonUtils` 或 `P` 前綴)。
// 原始類別:精簡為資料持有者
public class Polygon {
private List<Point> vertices;
// getter/setter...
}
// 工具類:封裝純演算法,零依賴、易測試
public final class PolygonUtils {
private PolygonUtils() {} // 防止實例化
public static int computePerimeter(Polygon p) { ... }
public static boolean isConvex(Polygon p) { ... }
public static boolean containsPoint(Polygon p, Point pt) { ... }
}
// 客戶端調用
int peri = PolygonUtils.computePerimeter(myPolygon);
優勢: 單一職責、無副作用、可獨立單元測試、支援函數式風格組合。
痛點: 方法簽名綁定具體類別 `Rectangle`,無法處理 `Circle`、`Triangle` 等同樣擁有「邊界矩形」概念的物件。
解法: 定義 `Rectangular` 介面,方法參數改為介面類型。
public interface Rectangular {
Rectangle getBounds(); // 契約:能提供邊界矩形
}
// 實作方
public class Circle implements Rectangular { ... }
public class Sprite implements Rectangular { ... }
// 演算法方法:完全不知情具體類別
public static boolean contains(Rectangular r, int x, int y) {
return r.getBounds().contains(x, y);
}
// 集合場景更顯威力
public static boolean anyOverlap(Collection<? extends Rectangular> shapes) { ... }
關鍵: 介面應盡量精簡 (Interface Segregation Principle),只定義演算法真正需要的契約。
痛點: `Window` 介面龐大,`areOverlapping(Window, Window)` 實際只需座標資訊,卻強迫呼叫端提供完整 `Window` 實作。
解法: 參數類型降級為最小介面 `Rectangular`,甚至為特定演算法定義專用單方法介面 (SAM / 函數式介面)。
// 排序演算法只需要「比較」能力
@FunctionalInterface
public interface Comparator<T> {
int compare(T a, T b);
}
public static <T> void sort(List<T> list, Comparator<T> cmp) { ... }
// 調用時傳入 Lambda,零負擔
sort(list, (a, b) -> a.getWeight() - b.getWeight());
| 策略 | 適用場景 | 核心原則 | 代價 |
|---|---|---|---|
| 靜態工具類 | 無狀態演算法、數學計算、字串處理 | 純函數、無副作用 | 失去多態擴展點 |
| 參數介面化 | 多型處理、集合操作、框架擴展點 | 依賴倒置、里氏替換 | 介面爆炸風險 |
| 最小介面 | 高階函數、策略模式、回調 | 介面隔離、角色導向 | 需設計良好契約 |
這三大策略並非互斥,而是層層遞進:先提煉純函數 (策略一),再以介面解耦參數 (策略二),最後精簡介面至最小集 (策略三)。配合現代 Java 的 Lambda、Stream、Record,能構建出極具彈性的代碼庫。