當客戶端用顯式型別檢查(chained if-elseswitch)取代多型呼叫——即使型別變化已封裝在階層中——就出現 Unexploited Encapsulation。

核心定義#

階層原本就用來封裝型別變化、讓多型自動分派。客戶端若繞開階層、改用顯式型別檢查,等於沒利用到既有的封裝。

副作用:

  • 緊耦合:客戶端綁定具體型別;新增型別必須同步更新 if-elseswitch
  • 遺漏導致 bug:忘了檢查階層中某個型別會在執行期出錯——多型機制下根本不會發生。

常見成因#

  • OO 中的程序式思維:C/Pascal 不支援執行期多型,習慣以型別編碼搭配 switchif-else,被帶進 OO 程式碼。
  • 未確實掌握 OO 原則:知道有階層與多型,但不會用它們建構高品質設計。

範例:JDK DataBuffer 階層#

JDK 的 DataBuffer 抽象類別有 DataBufferByteDataBufferDoubleDataBufferFloatDataBufferIntDataBufferShortDataBufferUShort 等子類別,分別包裝不同原始型別的資料陣列。同時 DataBuffer 也定義常數 TYPE_BYTETYPE_DOUBLETYPE_FLOAT 等。

Figure 4.17: 對 DataBuffer 階層使用顯式型別檢查

java.awt.image 套件中,這些常數被廣泛用於 switch 顯式型別檢查:

  • ComponentColorModel 有 16 個 switch
  • Raster 有 13 處
  • DirectColorModel 有 11 處
  • BandedSampleModelSampleModelColorModel 等也都有
switch (transferType) {
    case DataBuffer.TYPE_BYTE:
        byte bdata[] = (byte[])inData;
        pixel = bdata[0] & 0xff;
        length = bdata.length;
        break;
    case DataBuffer.TYPE_USHORT:
        short sdata[] = (short[])inData;
        pixel = sdata[0] & 0xffff;
        length = sdata.length;
        break;
    case DataBuffer.TYPE_INT:
        int idata[] = (int[])inData;
        pixel = idata[0];
        length = idata.length;
        break;
    default:
        throw new UnsupportedOperationException(...);
}

transferType 是 int,switch 完全靠編碼的型別常數——明顯未利用既有的 DataBuffer 階層

建議重構#

把分支動作搬進階層,改用多型方法。

範例的具體重構#

  • ColorModel 中的 protected int transferType 改成 protected DataBuffer dataBuffer
  • DataBuffer 階層中為各子類別定義對應的 getPixel()getSize()
  • 整段 switch 縮減為兩行:
pixel = dataBuffer.getPixel();
length = dataBuffer.getSize();

客戶端建立 ColorModel 時,需依輸入決定要傳入哪種 DataBuffer——這個 switch 集中到 client 或 factory 一處即可,而非散落整個程式碼。

軼事:被組織結構逼出的型別檢查#

一家電信公司中,PulsePhone 階層(CellPhoneLandPhone)原本透過多型計算費率。後來新需求要求 CellPhone 計算上網費——但 Phone 階層由 Device Team 擁有、Pulse 由 Billing Team 擁有,兩隊跨大洲分布。Device Team backlog 太多無法及時改動,Billing Team 只好在 PulsePhone 做型別檢查直接呼叫 CellPhone 的方法。

兩個教訓:

  • 組織結構會把壞味道擠進設計——這也是 Conway’s Law 的具體呈現。
  • 緊急工作流的快速妥協必須留下後續清債的書面追蹤,否則永遠回不來。

對品質屬性的衝擊#

  • 可變更性與可擴充性:型別變動時所有 switch 都要改。
  • 可重用性:行為散在客戶端,型別本身不再是「自足的可重用單位」。
  • 可測試性:相同檢查程式碼複製多份,測試成本上升。
  • 可靠性:忘了某個型別檢查 → 執行期錯誤。

別名#

  • Simulated polymorphism:用條件檢查模擬多型。
  • Improper use of switch statement:以 switch 取代多型分派。
  • instanceof checks:程式碼中 instanceof 過度集中。