在咱們平時的開發中,if else是最經常使用的條件判斷語句。在一些簡單的場景下,if else用起來很爽,可是在稍微複雜一點兒的邏輯中,大量的if else就會讓別人看的一臉蒙逼。
前端
若是別人要修改或者新增一個條件,那就要在這個上面繼續增長條件。這樣惡性循環下去,本來只有幾個if else最後就有可能變成十幾個,甚至幾十個。
別說不可能,我就見過有人在React組件裏面用了大量的if else,可讀性和可維護性很是差。(固然,這個不算if else的鍋,主要是組件設計的問題)java
這篇文章主要參與自《代碼大全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];
複製代碼
看完上面的例子,相信你對錶驅動法有了必定地認識。這裏引用一下《代碼大全》中的總結。bash
表驅動法就是一種編程模式,從表裏面查找信息而不使用邏輯語句。事實上,凡是能經過邏輯語句來選擇的事物,均可以經過查表來選擇。對簡單的狀況而言,使用邏輯語句更爲容易和直白。但隨着邏輯鏈的愈來愈複雜,查表法也就愈發顯得更具吸引力。優化
使用表驅動法前須要思考兩個問題,一個是如何從表中查詢,畢竟不是全部場景都像上面那麼簡單的,若是if判斷的是不一樣的範圍,這該怎麼查?
另外一個則是你須要在表裏面查詢什麼,是數據?仍是動做?亦或是索引?
基於這兩個問題,這裏將查詢分爲如下三種:ui
咱們上面介紹的那個日曆就是一個很好的直接訪問表的例子,可是不少狀況並無這麼簡單。spa
假設你在寫一個保險費率的程序,這個費率會根據年齡、性別、婚姻狀態等不一樣狀況變化,若是你用邏輯控制結構(if、switch)來表示不一樣費率,那麼會很是麻煩。設計
if (gender === 'female') {
if (hasMarried) {
if (age < 18) {
//
} else {
//
}
} else if (age < 18) {
//
} else {
//
}
} else {
...
}
複製代碼
可是從上面的日曆例子來看,這個年齡倒是個範圍,不是個固定的值,無法用數組或者對象來作映射,那麼該怎麼辦呢?這裏涉及到了上面說的問題,如何從表中查詢?
這個問題能夠用階梯訪問表和直接訪問表兩種方法來解決,階梯訪問這個後續會介紹,這裏只說直接訪問表。
有兩種解決方法:
一、複製信息從而可以直接使用鍵值
咱們能夠給1-17年齡範圍的每一個年齡都複製一份信息,而後直接用age來訪問,同理對其餘年齡段的也都同樣。這種方法在於操做很簡單,表的結構也很簡單。但有個缺點就是會浪費空間,畢竟生成了不少冗餘信息。
二、轉換鍵值
咱們不妨再換種思路,若是咱們把年齡範圍轉換成鍵呢?這樣就能夠直接來訪問了,惟一須要考慮的問題就是年齡如何轉換爲鍵值。
咱們固然能夠繼續用if else完成這種轉換。前面已經說過,簡單的if else是沒什麼問題的,表驅動只是爲了優化複雜的邏輯判斷,使其變得更靈活、易擴展。
enum ages {
unAdult = 0
adult = 1
}
enum genders {
female = 0,
male = 1
}
enum marry = {
unmarried = 0,
married = 1
}
const age2key = (age: number): string => {
if (age < 18) {
return ages.unAdult
}
return ages.adult
}
type premiumRateType = {
[ages: string]: {
[genders: string]: {
[marry: string]: {
rate: number
}
}
}
}
const premiumRate: premiumRateType = {
[ages.unAdult]: {
[genders.female]: {
[marry.unmarried]: {
rate: 0.1
},
[marry.married]: {
rate: 0.2
}
},
[genders.male]: {
[marry.unmarried]: {
rate: 0.3
},
[marry.married]: {
rate: 0.4
}
}
},
[ages.adult]: {
[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]
}
複製代碼
這樣,一旦判斷條件出現了變化,這裏只須要修改premiumRate裏面的數據就行了。
可是以爲這個例子舉得仍是不夠好,後續又想了一些方法來優化,將代碼修改成以下會更容易理解一些。
enum ages {
unAdult = 0,
adult = 1
}
enum genders {
female = 0,
male = 1
}
enum marry = {
unmarried = 0,
married = 1
}
const age2key = (age: number): string => {
if (age < 18) {
return ages.unAdult
}
return ages.adult
}
const rates: number[] = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
const premiumRate = {
0: [age.unAdult, genders.female, marry.unmarried],
1: [age.unAdult, genders.female, marry.married],
2: [age.unAdult, genders.male, marry.unmarried],
3: [age.unAdult, genders.male, marry.unmarried],
4: [age.adult, genders.female, marry.unmarried],
5: [age.adult, genders.female, marry.married],
6: [age.adult, genders.male, marry.unmarried],
7: [age.adult, genders.male, marry.unmarried]
}
type BoolCode = 0 | 1
const getRate = (age: number, hasMarried: BoolCode, gender: BoolCode) => {
const ageKey: BoolCode = age2key(age)
let index: string = ''
Object.keys(premiumRate).forEach((key, i) => {
const condition: BoolCode[] = premiumRate[key]
if (condition[0] === ageKey
&& condition[1] === gender
&& condition[2] === hasMarried
) {
index = key;
}
})
return rates[index];
}
複製代碼
這樣修改後,結構更加清晰,也更容易維護。
咱們前面那個保險費率問題,在處理年齡範圍的時候很頭疼,這種範圍每每不像上面那麼容易獲得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];
}
複製代碼
階梯訪問適合在索引訪問沒法適用的場景,好比若是是浮點數,就沒法用索引訪問建立一個數組來拿到索引。
在數據量比較大的狀況下,考慮用二分查找來代替順序查找,。
在大多數狀況下,優先使用直接訪問和索引訪問,除非二者實在沒法處理,才考慮使用階梯訪問。
從這三種訪問表來看,主要是爲了解決如何從表中查詢,在不一樣的場景應該使用合適的訪問表。
表驅動的意義是將數據和邏輯剝離,在開發中,直接修改配置比修改邏輯要更加安全。數據的添加、刪除比邏輯條件的添加、刪除風險更低,數據來源也更加靈活。
引用知乎大V Ivony的一段話:
分析和閱讀一段代碼的時候,不少時候是有側重面的,有時候側重於數據,有時候側重於邏輯。假設咱們有這樣一個需求,當某某值小於100時,就如何如何。那這個裏面的100就是數據,當需求變動爲某某值小於200時,才如何如何,那麼咱們關注的點在於這個數據的修改。而不是整個邏輯的修改,數據的剝離,有助於咱們更快的發現修改點和修改代碼。
參考資料:
PS:歡迎你們關注個人公衆號【前端小館】,你們一塊兒來討論技術。