表格驅動法(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)查詢索引,索引再指向主資料表的實際項目。
索引存取有三大優點:
- 節省空間:若主表每筆資料很大,用小型索引陣列比在主表中保留大量空位划算得多。例如 10,000 個索引(每筆 2 bytes)加 100 筆主資料(每筆 100 bytes)只需 30,000 bytes,遠少於直接建立 10,000 筆主資料所需的 1,000,000 bytes。
- 多種排序檢視:可以針對同一份主表建立多個索引(按員工姓名、入職日期、薪資等),操作索引比操作主表便宜。
- 易於維護與替換:將索引存取邏輯封裝在獨立常式中,日後要更換存取策略時更容易。
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 節)
查核表:表格驅動法
- 是否考慮過以表格驅動法取代複雜的邏輯?
- 是否考慮過以表格驅動法取代複雜的繼承結構?
- 是否考慮過將表格資料存放在外部檔案,在執行時讀取,以便在不修改程式碼的情況下變更資料?
- 若表格無法以簡單的陣列索引直接存取,是否已將索引鍵的計算封裝在獨立常式中,而非在程式碼各處重複計算?
要點#
- 表格提供了複雜邏輯和繼承結構的替代方案。若程式邏輯或繼承樹令你困惑,問問自己能否用查找表來簡化。
- 使用表格的關鍵考量之一是如何存取表格:直接存取、索引存取或階梯存取。
- 另一個關鍵考量是表格中該放什麼:資料或動作。