當設計中引入了原本可避免的抽象——責任瑣碎、甚至完全沒有責任——就出現 Unnecessary Abstraction 壞味道。
核心定義#
「指派單一且有意義的責任」是抽象的使能技巧之一。當抽象只是為了「方便」或「為了建而建」而存在,就違反這個技巧。
常見成因#
- OO 中的程序式思維:類別淪為「動詞 + 資料」的拼裝。
- 為了便利而濫用語言特性:例如以「常數介面(constant interface)」取代列舉,省掉每次以型別名稱限定的麻煩。
- 過度設計(over-engineering):為了單純的資料元素硬建類別,例如為
Customer中的 customer ID 建立CustomerID類別。
範例#
範例 1:FormattableFlags 的常數類別#
java.util.FormattableFlags(JDK 7)只用來放三個整數旗標:
public class FormattableFlags {
private FormattableFlags() {}
public static final int LEFT_JUSTIFY = 1<<0;
public static final int UPPERCASE = 1<<1;
public static final int ALTERNATE = 1<<2;
}此類別在 Java 1.5 引入時,列舉(enum)已經存在——應該直接使用 enum。
範例 2:常數介面#
javax.swing.WindowConstants 將四個視窗關閉操作的常數定義在介面裡:
public interface WindowConstants {
public static final int DO_NOTHING_ON_CLOSE = 0;
public static final int HIDE_ON_CLOSE = 1;
public static final int DISPOSE_ON_CLOSE = 2;
public static final int EXIT_ON_CLOSE = 3;
}JFrame、JInternalFrame、JDialog 等類別繼承此介面,只為了方便存取常數。書中強烈反對這種做法:
- 子類別被「汙染」上不相關的常數。
- 常數本是實作細節,透過介面公開等於違反封裝。
- 介面中的常數一改,所有客戶端都會壞。
介面的本意是「實作者必須遵守的協定」。把它當常數容器是對抽象機制的濫用。
範例 3:BestSellerBook 的純委派#
電商系統建立 BestSellerBook 類別,但它所有方法都委派給 Book,沒有額外行為——這個抽象沒理由存在。
建議重構#
「對自己付不出代價的類別應該被消滅。」(Fowler)
- 套用 inline class 重構,將該類別合併進另一個類別。
- 若用類別/介面承載常數,改用 enum。
- 範例 1:將
FormattableFlags變為 enum,搭配java.util.Formatter使用。 - 範例 2:將
WindowConstants改為 enum;ObjectStreamConstants同理。 - 範例 3:刪除
BestSellerBook,在Book中加上isBestSeller屬性與其 getter/setter。
對品質屬性的衝擊#
- 理解性:多餘的抽象增加認知負擔。
- 可重用性:責任瑣碎或無責任的抽象難在新脈絡中重用。
別名#
- Irrelevant class:類別在設計中無有意義的行為。
- Lazy class/Freeloader:類別做的事「太少」。
- Small class、Mini-class:方法/欄位過少。
- No responsibility:類別沒有任何責任。
- Agent classes:類別只是訊息傳遞者。
實務考量#
設計樣式中的委派類別#
Mediator、Proxy、Façade、Adapter 等樣式可能讓類別「看起來在純委派」。但這些類別有清楚明確的責任(如 Adapter 把 Adaptee 的介面轉成客戶需要的介面),不算 Unnecessary Abstraction。
容納變化#
java.lang.Math 與 java.lang.StrictMath 都提供類似的數學函式:
Math為了效能,可能用原生硬體浮點。StrictMath嚴格遵守浮點標準,跨平台結果一致。- 兩者並存是有意識的設計選擇——以兩個抽象容納可攜性 vs. 效能的權衡,不是壞味道。