當設計中用一團原始資料或編碼字串取代了一個本應存在的類別/介面時,就出現 Missing Abstraction 壞味道。

核心定義#

抽象的使能技巧之一是「提供清晰的概念邊界與唯一身份」。當這個身份消失,被原始型別(primitive types)或字串取代時,就違反了抽象原則。

連帶副作用:

  • 違反封裝:原本應隱藏在抽象內的實作細節被外露。
  • 違反模組化:相關的資料與行為被散落到多個抽象中,造成緊耦合與脆弱不可重用的程式碼。

常見成因#

  • 設計分析不足:在期限壓力下「先讓程式跑起來」,順手用原始型別或字串敷衍。
  • 缺乏重構:早期用字串/原始型別表示的概念,後來成長為複雜邏輯卻沒有及時重構。
  • 過度迷戀微幅效能:誤以為陣列存取比物件成員存取快,因而避開應有的抽象。多數情境下,這點效能差距遠不足以彌補設計品質的損失。

範例#

範例 1:ISBN#

在圖書館管理系統中:

  • ISBN 有 10 碼與 13 碼兩種形式,可互轉。
  • 每個位元都有意義(前綴、註冊群、註冊者、出版品、檢核碼)。
  • 檢核碼依特定演算法計算,可驗證號碼正確性。
  • 可轉換為條碼。

若以字串或長整數儲存 ISBN,驗證/轉換/比對等邏輯就會散落並重複——這是典型的 Missing Abstraction。

範例 2:資料叢集(data clumps)#

繪圖程式中的「矩形選取區」常被表示為:

  • (x1, y1)(x2, y2) 兩組 double,或
  • (x1, y1) 加上 (width, height)

這些值總是一起出現、一起傳遞,是典型的 data clumps——同樣是 Missing Abstraction。

範例 3:JDK 的 stack trace#

Java 1.0 的 Throwable.printStackTrace() 把整個 stack trace 編碼為字串輸出。客戶端為了取得行號或判斷重複錯誤,必須去剖析字串格式。一旦客戶端依賴此格式:

  • 設計者就不能變更字串格式。
  • 設計被字串「鎖死」。

建議重構#

為原始資料或字串建立真正的抽象,把建構、驗證、複製等相關行為移入。

  • 範例 1:建立 ISBN 抽象類別/介面,並衍生 ISBN10ISBN13 子類別。

Figure 3.4: ISBN 範例的建議重構

  • 範例 2:建立 RectangleSelectedRegion 類別,把欄位與相關方法移入。
  • 範例 3:JDK 1.4 補上 StackTraceElement 類別,提供程式化存取;既有 printStackTrace() 為了相容仍保留。
public final class StackTraceElement {
    public String getFileName();
    public int getLineNumber();
    public String getClassName();
    public String getMethodName();
    public boolean isNativeMethod();
}

對品質屬性的衝擊#

  • 理解性:核心概念散落在程式碼基底中,難以追蹤。
  • 可變更性與可擴充性:找出該改的地方都困難,更別說避免連鎖效應。
  • 可重用性與可測試性:對應領域實體的邏輯沒有單一收斂點,難以重用、難以測試。
  • 可靠性:失去抽象作為「資料完整性守門員」的角色,正確性容易被破壞。

別名#

  • Primitive obsession:以原始型別表示日期、貨幣等。
  • Data clumps:總是一起出現的資料叢集。

實務考量#

並非每個資料都需要抽象化——有些只是「純資料元素」,沒有額外行為。

判斷是否該建立抽象的快速檢核:

  • 是否需要透過建構式做預設初始化?
  • 是否需要驗證?
  • 是否需要美觀輸出(pretty printing)?
  • 是否需要釋放資源?

任何一項為「是」,就應該認真考慮建立抽象,而非過度追求精簡。