表格驅動法(Table-Driven Method)是一種以「查表」取代邏輯判斷(if/case)的技巧。幾乎所有能用邏輯語句選擇的事物,都可以改用表格來選擇。在簡單情境下,邏輯語句更直覺;但當邏輯鏈變得複雜時,表格的優勢就會越來越明顯。

18.1 表格驅動法使用總則#

使用表格驅動法時,需要解決兩個核心問題:

如何查詢表格中的項目#

根據資料特性的不同,有三種存取方式:

  • 直接存取(Direct Access):以資料直接作為索引鍵,例如用月份數字直接查表。
  • 索引存取(Indexed Access):先用資料查索引表取得鍵值,再用鍵值查主表。
  • 階梯存取(Stair-Step Access):資料落在某個範圍內,逐級比對後決定對應的類別。
flowchart TD
    Q1{"資料可直接當索引?"}
    Q2{"需先查索引表?"}

    R1["直接存取"]
    R2["索引存取"]
    R3["階梯存取"]

    Q1 -- "是" --> R1
    Q1 -- "否" --> Q2
    Q2 -- "是" --> R2
    Q2 -- "否,資料落在範圍中" --> R3

表格中該存放什麼#

  • 若查表結果是資料,直接存放資料即可。
  • 若查表結果是動作,可以存放動作代碼,或在支援的語言中存放函式參考(Function Reference)。

18.2 直接存取表#

直接存取表的特點是不需要任何中間步驟,直接用資料當索引取得結果。

每月天數範例#

用冗長的 if 來判斷每月天數既笨拙又難維護。改用陣列查表後:

Dim daysPerMonth() As Integer = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
days = daysPerMonth( month - 1 )

若要支援閏年,只需將陣列擴展為二維,邏輯依然簡潔。

保險費率範例#

保險費率可能依據年齡、性別、婚姻狀態、是否吸菸等多重因素而異。若用邏輯判斷會產生大量巢狀 if;改用多維陣列後,只需一行查表:

rate = rateTable( smokingStatus, gender, maritalStatus, age )

將程式的知識放進資料而非邏輯中,讓程式碼更易讀也更容易修改。

彈性訊息格式範例#

當資料格式過於動態,無法以硬編碼的 if 描述時,表格驅動法的威力尤為突出。以浮標監測訊息為例:約 20 種訊息類型,每種有不同欄位組合。

  • 邏輯導向做法:為每種訊息寫一個專屬處理常式,共需 20 個。
  • 物件導向做法:為每種訊息建立子類別,本質上同樣需要 20 個類別,複雜度並未降低。
  • 表格驅動做法:將每種訊息的欄位描述(欄位名稱與資料型別)存入表格,只需一個通用常式依據表格描述來讀取並印出任何訊息。
flowchart LR
    subgraph S1["邏輯導向"]
        direction TB
        L1["if / case 分支"] --> L2["處理常式 1"]
        L1 --> L3["處理常式 2"]
        L1 --> L4["..."]
        L1 --> L5["處理常式 20"]
    end

    subgraph S2["物件導向"]
        direction TB
        O1["基底類別 Message"] --> O2["子類別 1"]
        O1 --> O3["子類別 2"]
        O1 --> O4["..."]
        O1 --> O5["子類別 20"]
    end

    subgraph S3["表格驅動"]
        direction TB
        T1["訊息描述表格"] --> T2["1 個通用處理常式"]
        T2 --> T3["依表格描述讀取並輸出"]
    end

可進一步搭配多型(Polymorphism):建立抽象欄位類別 AbstractField,針對每種資料型別(浮點數、整數、字串等)建立子類別,再將子類別物件存入陣列。如此連 case 語句都可以省略,只需一行 field[fieldType].ReadAndPrint() 即可。

這種做法的關鍵設計洞見不在於物件導向或函式導向,而在於精心設計的查找表。若訊息描述從檔案讀入,新增訊息類型甚至不需要改動程式碼。

調整查詢鍵(Fudging Lookup Keys)#

當資料無法直接當索引時,有幾種處理策略:

  • 複製資料:例如將 0-17 歲的費率複製到每個年齡欄位,使年齡可直接作為索引。好處是查表簡單,壞處是浪費空間且有資料不一致的風險。
  • 轉換鍵值:對資料施加函式轉換,例如 max(min(66, age), 17) 將年齡限制在 17-66 的範圍內。
  • 將轉換封裝在獨立常式中:例如建立 KeyFromAge() 函式,避免在多處重複轉換邏輯,也方便日後修改。

若程式語言或平台已提供現成的鍵值轉換機制(如 Java 的 HashMap),應優先使用。

18.3 索引存取表#

當簡單的數學轉換不足以將資料對應到表格索引時,可使用索引存取法。

做法是建立一個索引陣列,用原始資料(如零件編號 0000-9999)查詢索引,索引再指向主資料表的實際項目。

索引存取有三大優點:

  1. 節省空間:若主表每筆資料很大,用小型索引陣列比在主表中保留大量空位划算得多。例如 10,000 個索引(每筆 2 bytes)加 100 筆主資料(每筆 100 bytes)只需 30,000 bytes,遠少於直接建立 10,000 筆主資料所需的 1,000,000 bytes。
  2. 多種排序檢視:可以針對同一份主表建立多個索引(按員工姓名、入職日期、薪資等),操作索引比操作主表便宜。
  3. 易於維護與替換:將索引存取邏輯封裝在獨立常式中,日後要更換存取策略時更容易。

18.4 階梯存取表#

階梯存取法適用於資料對應到範圍而非離散數值的情境。

成績分級範例#

假設評分標準為:>=90% 為 A、<90% 為 B、<75% 為 C、<65% 為 D、<50% 為 F。這種不規則的範圍無法用簡單函式轉換成索引,也不適合用索引法(因為分數是浮點數)。

改用階梯法,將每個範圍的上限存入表格,再用迴圈逐級比對:

Dim rangeLimit() As Double = { 50.0, 65.0, 75.0, 90.0, 100.0 }
Dim grade() As String =      { "F",  "D",  "C",  "B",  "A"   }

這種做法在面對不規則數據(如機率分布 0.458747、0.547651 等)時尤其有效。

使用階梯法的注意事項#

  • 留意端點處理:確保範圍的上下界都被正確處理,特別注意 <<= 的差異。
  • 考慮二分搜尋:當範圍列表很長時,循序搜尋的成本可能過高,可改用準二分搜尋(Quasi-Binary Search)。此時需特別處理端點的特殊情況。
  • 考慮改用索引存取:若執行速度是關鍵考量,可用索引表取代階梯法,以空間換取時間。
  • 封裝為獨立常式:將階梯查表邏輯放入專屬函式中,方便日後修改。

多種方案都可行時,不必執著於找出「最佳」解。正如 Butler Lampson 所言:追求好的方案並避開災難,比追求最佳方案更重要。

18.5 表格查詢的其他範例#

本書其他章節也應用了表格查詢技巧:

  • 保險費率查表(16.3 節)
  • 以決策表取代複雜邏輯(19.1 節)
  • 表格查詢時的記憶體分頁成本(25.3 節)
  • 布林值組合的查表簡化(26.1 節)
  • 貸款還款表的預先計算(26.4 節)
查核表:表格驅動法
  • 是否考慮過以表格驅動法取代複雜的邏輯?
  • 是否考慮過以表格驅動法取代複雜的繼承結構?
  • 是否考慮過將表格資料存放在外部檔案,在執行時讀取,以便在不修改程式碼的情況下變更資料?
  • 若表格無法以簡單的陣列索引直接存取,是否已將索引鍵的計算封裝在獨立常式中,而非在程式碼各處重複計算?

要點#

  • 表格提供了複雜邏輯和繼承結構的替代方案。若程式邏輯或繼承樹令你困惑,問問自己能否用查找表來簡化。
  • 使用表格的關鍵考量之一是如何存取表格:直接存取、索引存取或階梯存取。
  • 另一個關鍵考量是表格中該放什麼:資料或動作。