當一個抽象同時擔負多個責任——有多個「面向」可以引發它變更——就出現 Multifaceted Abstraction 壞味道。
核心定義#
抽象的使能技巧之一是「指派單一且有意義的責任」。違反此技巧最直接的形式即為 SRP(單一職責原則)破洞:
- 同一個類別有多個互不相關的責任。
- 方法之間的內聚低。
- 類別會因為多種不同原因而被修改。
經驗顯示:修改頻率高的元素往往缺陷也較多。Multifaceted Abstraction 是最常見的「缺陷溫床」之一。
常見成因#
- 泛用名稱(general-purpose abstractions):類別取名為
Node、Component、Element、Item等,最後變成「萬用收納箱」。 - 長期演化沒有重構:每次需求變更都往同一個類別塞一點,最終變成 monolithic blob。
- 流程黏滯帶來的逃避:建新類別在組織內成本太高,於是就硬塞進現有類別。
- 混合關注點:未分離 domain logic 與 presentation logic,等等。
範例:java.util.Calendar#
書中經典案例:java.util.Calendar。
- 名稱表明處理日期,但類別也處理時間。
- 介面龐大、難以理解,催生了 Joda-Time
↗、Java 8 的
java.time套件等替代方案。 - JDK 7 中該類別有 2825 行、67 個方法、71 個欄位!
大型/複雜不是 Multifaceted Abstraction 的必要條件——細小但責任重疊也算。詳見 5.2 節 Insufficient Modularization。
軼事:Image 類別的爭辯#
一個手持裝置的圖形顯示專案中,
Image類別有 120 個 public methods、超過 50,000 行程式碼。
開發團隊堅稱:「全部都是與 image 相關,所以遵守 SRP。」這正是常見的迷思:
「責任」不等於「邏輯上相關的所有功能」。責任是『一個變更原因』——必須具體、明確、精準。
進一步追問後,團隊主管承認 Image 是專案中最常被修改的類別,且修改原因彼此無關。最終把它拆成 ImageLoader、ImageRenderer、ImageProcessor、ImageWriter 四個模組。重構帶來最大好處:變更被在地化——例如所有 rendering 改動只動 ImageRenderer。
建議重構#
找出方法/欄位的邏輯叢集,套用 extract class 重構。
- 將
Calendar中時間相關的方法與欄位抽到新的Time類別中。 - Java 8 的
java.time套件正是這個方向的官方實踐。
flowchart LR
A[Multifaceted Class<br/>多重責任] -->|extract class| B[Class A<br/>責任 1]
A --> C[Class B<br/>責任 2]
A --> D[Class C<br/>責任 3]
Figure 3.11: 針對 Multifaceted Abstraction 套用 Extract Class 重構
對品質屬性的衝擊#
- 理解性:多重責任 → 認知負擔上升。
- 可變更性與可擴充性:難以決定該動哪些成員;改動可能波及無關責任。
- 可重用性:要重用一個責任就得把全部責任搬走;責任糾纏時行為不可預測。
- 可測試性:責任互相耦合,難以分離測試。
- 可靠性:糾纏的責任造成意外副作用。
別名#
- Divergent change:類別因不同原因而被修改。
- Conceptualization abuse:兩個或多個不內聚的概念塞進同一類別。
- Large class:類別擁有過多責任。
- Lack of cohesion:低內聚的「kitchen sink」型類別。