在我們平時的開發中,if else是最常用的條件判斷語句。在一些簡單的場景下,if else用起來很爽,但是在稍微複雜一點兒的邏輯中,大量的if else就會讓別人看的一臉蒙逼。
如果別人要修改或者新增一個條件,那就要在這個上面繼續增加條件。這樣惡性迴圈下去,原本只有幾個if else最後就有可能變成十幾個,甚至幾十個。
別說不可能,我就見過有人在React元件裡面用了大量的if else,可讀性和可維護性非常差。(當然,這個不算if else的鍋,主要是元件設計的問題)
這篇文章主要參與自《程式碼大全2》,原書中使用vb和java實現,這裡我是基於TypeScript的實現,對書中內容加入了一些自己的理解。
從一個例子說起
日曆
假如我們要做一個日曆元件,那我們肯定要知道一年12個月中每個月都多少天,這個我們要怎麼判斷呢?
最笨的方法當然是用if else啊。
if (month === 1) {
return 31;
}
if (month === 2) {
return 28;
}
...
if (month === 12) {
return 31;
}
這樣一下子就要寫12次if,白白浪費了那麼多時間,效率也很低。
這個時候就會有人想到用switch/case來做這個了,但是switch/case也不會比if簡化很多,依然要寫12個case啊!!!甚至如果還要考慮閏年呢?豈不是更麻煩?
我們不妨轉換一下思維,每個月份對應一個數字,月份都是按順序的,我們是否可以用一個陣列來儲存天數?到時候用下標來訪問?
const month: number = new Date().getMonth(),
year: number = new Date().getFullYear(),
isLeapYear: boolean = year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
const monthDays: number[] = [31, isLeapYear ? 29 : 28, 31, ... , 31];
const days: number = monthDays[month];
概念
看完上面的例子,相信你對錶驅動法有了一定地認識。這裡引用一下《程式碼大全》中的總結。
表驅動法就是一種程式設計模式,從表裡面查詢資訊而不使用邏輯語句。事實上,凡是能通過邏輯語句來選擇的事物,都可以通過查表來選擇。對簡單的情況而言,使用邏輯語句更為容易和直白。但隨著邏輯鏈的越來越複雜,查表法也就愈發顯得更具吸引力。
使用表驅動法前需要思考兩個問題,一個是如何從表中查詢,畢竟不是所有場景都像上面那麼簡單的,如果if判斷的是不同的範圍,這該怎麼查?
另一個則是你需要在表裡面查詢什麼,是資料?還是動作?亦或是索引?
基於這兩個問題,這裡將查詢分為以下三種:
- 直接訪問
- 索引訪問
- 階梯訪問
直接訪問表
我們上面介紹的那個日曆就是一個很好的直接訪問表的例子,但是很多情況並沒有這麼簡單。
統計保險費率
假設你在寫一個保險費率的程式,這個費率會根據年齡、性別、婚姻狀態等不同情況變化,如果你用邏輯控制結構(if、switch)來表示不同費率,那麼會非常麻煩。
if (gender === `female`) {
if (hasMarried) {
if (age < 18) {
//
} else if (age < 65) {
//
} else {
//
}
} else if (age < 18) {
//
} else if (age < 65) {
//
} else if {
//
}
} else {
...
}
但是從上面的日曆例子來看,這個年齡卻是個範圍,不是個固定的值,沒法用陣列或者物件來做對映,那麼該怎麼辦呢?這裡涉及到了上面說的問題,如何從表中查詢?
這個問題可以用階梯訪問表和直接訪問表兩種方法來解決,階梯訪問這個後續會介紹,這裡只說直接訪問表。
有兩種解決方法:
1、複製資訊從而能夠直接使用鍵值
我們可以給1-17年齡範圍的每個年齡都複製一份資訊,然後直接用age來訪問,同理對其他年齡段的也都一樣。這種方法在於操作很簡單,表的結構也很簡單。但有個缺點就是會浪費空間,畢竟生成了很多冗餘資訊。
2、轉換鍵值
我們不妨再換種思路,如果我們把年齡範圍轉換成鍵呢?這樣就可以直接來訪問了,唯一需要考慮的問題就是年齡如何轉換為鍵值。
我們當然可以繼續用if else完成這種轉換。前面已經說過,簡單的if else是沒什麼問題的,表驅動只是為了優化複雜的邏輯判斷,使其變得更靈活、易擴充套件。
enum genders {
lessThan18 = `<18`,
between18And56 = `18-65`,
moreThan56 = `>65`
}
enum genders {
female = 0,
male = 1
}
enum marry = {
unmarried = 0,
married = 1
}
const age2key = (age: number): string => {
if (age < 18) {
return genders.lessThan18
}
if (age < 65) {
return genders.between18And56
}
return genders.moreThan56
}
const premiumRate: {
[genders: string]: {
[marry: string]: {
rate: number
}
}
} = {
[genders.lessThan18]: {
[genders.female]: {
[marry.unmarried]: {
rate: 0.1
},
[marry.married]: {
rate: 0.2
}
},
[genders.male]: {
[marry.unmarried]: {
rate: 0.3
},
[marry.married]: {
rate: 0.4
}
}
},
[genders.between18And56]: {
[genders.female]: {
[marry.unmarried]: {
rate: 0.5
},
[marry.married]: {
rate: 0.6
}
},
[genders.male]: {
[marry.unmarried]: {
rate: 0.7
},
[marry.married]: {
rate: 0.8
}
}
},
[genders.moreThan56]: {
[genders.female]: {
[marry.unmarried]: {
rate: 0.5
},
[marry.married]: {
rate: 0.6
}
},
[genders.male]: {
[marry.unmarried]: {
rate: 0.7
},
[marry.married]: {
rate: 0.8
}
}
}
const getRate = (age: number, hasMarried: 0 | 1, gender: 0 | 1) => {
const ageKey: string = age2key(age);
return premiumRate[ageKey]
&& premiumRate[ageKey][gender]
&& premiumRate[ageKey][gender][hasMarried]
}
索引訪問表
我們前面那個保險費率問題,在處理年齡範圍的時候很頭疼,這種範圍往往不像上面那麼容易得到key。
我們當時提到了複製資訊從而能夠直接使用鍵值,但是這種方法浪費了很多空間,因為每個年齡都會儲存著一份資料,但是如果我們只是儲存索引,通過這個索引來查詢資料呢?
假設人剛出生是0歲,最多能活到100歲,那麼我們需要建立一個長度為101的陣列,陣列的下標對應著人的年齡,這樣在0-17的每個年齡我們都儲存`<18`,在18-65儲存`18-65`, 在65以上儲存`>65`。
這樣我們通過年齡就可以拿到對應的索引,再通過索引來查詢對應的資料。
看起來這種方法要比上面的直接訪問表更復雜,但是在一些很難通過轉換鍵值、資料佔用空間很大的場景下可以試試通過索引來訪問。
const ages: string[] = [`<18`, `<18`, `<18`, `<18`, ... , `18-65`, `18-65`, `18-65`, `18-65`, ... , `>65`, `>65`, `>65`, `>65`]
const ageKey: string = ages[age];
階梯訪問表
同樣是為了解決上面那個年齡範圍的問題,階梯訪問沒有索引訪問直接,但是會更節省空間。
為了使用階梯方法,你需要把每個區間的上限寫入一張表中,然後通過迴圈來檢查年齡所在的區間,所以在使用階梯訪問的時候一定要注意檢查區間的端點。
const ageRanges: number[] = [17, 65, 100],
keys: string[] = [`<18`, `18-65`, `>65`],
len: number = keys.length;
const getKey = (age: number): string => {
for (let i = 0; i < len; i++) {
console.log(`i`, i)
console.log(`ageRanges`, ageRanges[i])
if (age <= ageRanges[i]) {
return keys[i]
}
}
return keys[len-1];
}
階梯訪問適合在索引訪問無法適用的場景,比如如果是浮點數,就無法用索引訪問建立一個陣列來拿到索引。
在資料量比較大的情況下,考慮用二分查詢來代替順序查詢,。
在大多數情況下,優先使用直接訪問和索引訪問,除非兩者實在無法處理,才考慮使用階梯訪問。
從這三種訪問表來看,主要是為了解決如何從表中查詢,在不同的場景應該使用合適的訪問表。
參考資料: