當一個抽象同時擔負多個責任——有多個「面向」可以引發它變更——就出現 Multifaceted Abstraction 壞味道。

核心定義#

抽象的使能技巧之一是「指派單一且有意義的責任」。違反此技巧最直接的形式即為 SRP(單一職責原則)破洞:

  • 同一個類別有多個互不相關的責任
  • 方法之間的內聚低
  • 類別會因為多種不同原因而被修改。

經驗顯示:修改頻率高的元素往往缺陷也較多。Multifaceted Abstraction 是最常見的「缺陷溫床」之一。

常見成因#

  • 泛用名稱(general-purpose abstractions):類別取名為 NodeComponentElementItem 等,最後變成「萬用收納箱」。
  • 長期演化沒有重構:每次需求變更都往同一個類別塞一點,最終變成 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 是專案中最常被修改的類別,且修改原因彼此無關。最終把它拆成 ImageLoaderImageRendererImageProcessorImageWriter 四個模組。重構帶來最大好處:變更被在地化——例如所有 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」型類別。