當程式碼用**條件邏輯(switch、chained if-else)搭配「標籤型別」**處理行為的多樣性,而本應該以階層加封裝時,就出現 Missing Hierarchy。

核心定義#

最有名的設計壞味道之一就是「靠型別碼做 switch」。當把型別資訊編碼為 enum、整數或字串時:

  • 行為差異未被妥善封裝——子型別的存在感為零。
  • 共通性未被善用——通用部分散在各分支重寫。
  • 同樣的 switch/if-else 會在多處重複,違反 DRY。

這也是 4.4 節 Unexploited Encapsulation 的鄰居——差別在於 Unexploited Encapsulation 階層已經存在但被繞開;Missing Hierarchy 則是階層根本不存在

常見成因#

  • 被誤導的「簡單」:以為 enum + 條件式比類別階層直接。
  • 程序式取向:在 OO 語言中沿用 C 的型別編碼套路。
  • 忽視繼承的價值:缺乏對 OO 機制的訓練。

範例#

範例 1:XPStyle.getBorderInsets#

if (c instanceof AbstractButton) {
    margin = ((AbstractButton)c).getMargin();
} else if (c instanceof JToolBar) {
    margin = ((JToolBar)c).getMargin();
} else if (c instanceof JTextComponent) {
    margin = ((JTextComponent)c).getMargin();
}

程式碼註解裡作者就坦承:「Ideally we’d have an interface defined for classes which support margins (to avoid this hackery), but we’ve decided against it for simplicity」——同樣的 if-else 在同一檔案重複了三次。

範例 2:AudioFormat.Encoding#

public static class Encoding {
    public static final Encoding PCM_SIGNED = new Encoding("PCM_SIGNED");
    public static final Encoding PCM_UNSIGNED = new Encoding("PCM_UNSIGNED");
    public static final Encoding PCM_FLOAT = new Encoding("PCM_FLOAT");
    public static final Encoding ULAW = new Encoding("ULAW");
    public static final Encoding ALAW = new Encoding("ALAW");
    private String name;
}

客戶端到處出現比對:

if (targetEncoding.equals(Encoding.PCM_FLOAT)) bits = 32;
if (targetEncoding.equals(Encoding.PCM_SIGNED)) ...

建議重構#

若條件分支內呼叫共通方法 → 引入介面包裝該協定。 若條件分支可以對應為類別 → 套用 extract hierarchy 並用多型替代條件式。

範例 1 的重構#

Figure 6.4: 為 AbstractButton、JTextComponent、JToolBar 建議的重構

引入 MarginSupported 介面,讓 AbstractButtonJToolBarJTextComponent 實作:

if (c instanceof MarginSupported) {
    margin = c.getMargin();
}

理想上 getBorderInsets() 應直接以 MarginSupported 為參數型別,但因為公開 API 不能改簽章,仍須保留 instanceof MarginSupported

範例 2 的重構#

Figure 6.5: 為音訊格式範例建議的重構

建立子類別 SignedPCMEncodingUnsignedPCMEncodingFloatPCMEncodingULAWEncodingALAWEncoding 繼承抽象的 Encoding,並把 getBitSize() 等方法交給多型分派:

bits = targetEncoding.getBitSize();

對品質屬性的衝擊#

  • 理解性:條件邏輯比多型多餘且難以掌握。
  • 可變更性與可擴充性:每加一個型別都要改所有 switch。
  • 可重用性:型別與行為被切離,整體難以重用。
  • 可測試性:散落各處的條件式測試成本高。
  • 可靠性:忘了某個 case → 執行期錯誤;型別碼用字串/整數時還缺驗證。

別名#

  • Tag class:類別中含有「flavor」標籤欄位。
  • Missing inheritance:以重複程式碼或 switch-case 取代繼承。
  • Collapsed type hierarchy:類別中含有兩個以上的 switch/等價 if-else 來決定行為。
  • Embedded features:類別屬性是「可選功能的開關」。

實務考量:與外部世界互動#

檔案格式(如 .class)會在資料中加上型別標記。讀取時必須以 switch 或 if-else 解碼。 同理,Object Serialization 中 ObjectStreamField 的型別碼(ZBCS…)也是合理使用條件邏輯的場景。 資料一旦轉成物件後就不該再用 switch