課本

Code Complete 2nd-ch18.pdf


簡報 (08/26補)

CodeComplete_ch18_簡報.pdf


導讀者筆記

大綱


18.1 使用表格驅動法的注意事項

表格驅動法是什麼

簡單來說,表格驅動法是藉由直接在「表格」中尋找資訊,而不是用邏輯將其計算出來(if, case等)。

範例

當我們經營一間餐廳,每張桌子都有不同的座位數、沒有邏輯的時候,我們可能就會需要用到if-else來判斷:

if table number == 1
    table has 4 seats
else if table number == 2
    table has 8 seats
. . .

// 如果只有5張桌子,那或許可以這樣做。但是如果有50張桌子,這邏輯語言就會顯得冗長許多,維護起來也較困難。

因此作者用array(陣列)去當作一個表格驅動法來解:

tables [] = {4, 8, 2, 4, ...}
table seats = tables[table number]

// 將Index作為座位編號,值代表座位數。

這麼一來程式碼就會變得又簡單又短,效率高且方便維護。它是一種直接使用「物件結構」來展示結果的方式。

補充參考資料>>

What are table-driven methods?

假如想要依字元區分出數字、標點及字母,可以看看下面的範例:

// 區分字母
if ( ( ( 'a' <= inputChar ) && ( inputChar <= 'z' ) ) ||
		( ( 'A' <= inputChar ) && ( inputChar <= 'Z' ) ) ) {
		charType = CharacterType.Letter;
}
// 區分標點
else if ( ( inputChar == ' ' ) || ( inputChar == ',' ) ||
		( inputChar == '.' ) || ( inputChar == '!' ) || ( inputChar == '(' ) ||
		( inputChar == ')' ) || ( inputChar == ':' ) || ( inputChar == ';' ) ||
		( inputChar == '?' ) || ( inputChar == '-' ) ) {
		charType = CharacterType.Punctuation;
}
// 區分數字
else if ( ( '0' <= inputChar ) && ( inputChar <= '9' ) ) {
		charType = CharacterType.Digit;
}

如果改用表格查找,我們可以這樣簡化:

charType = charTypeTable[ inputChar ];

[補充] 我們將其放進我們的表格(這裡是以ASCII為例)

CharacterType[] charTypeTable = new CharacterType[128]; 

// 初始化查找的表格,用ASCII值判斷屬於哪種字符。
for (int i = 0; i < charTypeTable.length; i++) {
    if (i >= 'a' && i <= 'z' || i >= 'A' && i <= 'Z') {
        charTypeTable[i] = CharacterType.Letter;
    } else if (i >= '0' && i <= '9') {
        charTypeTable[i] = CharacterType.Digit;
    } else if (",.!?():-;".indexOf(i) >= 0) {   // indexOf查無相符結果會回傳-1
        charTypeTable[i] = CharacterType.Punctuation;
    } else {
        charTypeTable[i] = CharacterType.Other; // 其他字符類型
    }
}

使用表格驅動會發生的兩個問題

  1. 找到查找表格的方式。例如12月份可以用Array的索引。但如果資料較複雜或難以處理,無法簡單的去放進表格,那你可以考慮接下來的章節要介紹的方式:
  2. 定位你要儲存在表格內的資料。通常情況下,你可以直接把「值」放進去。另一種情況則是將「操作(action)」放進表格內,也就是說你可以將執行的函數或其他程式設定為查詢的結果。

上述不論是哪一種,都會增加表格的複雜性。

18.2 直接存取 Direct Access Tables

與所有查詢表一樣,直接存取表代替了更複雜的邏輯控制,不必經過任何複雜的障礙,就可以找到所需要的資料。

顧名思義,直接存取表可以直接訪問指定的表格元素

顧名思義,直接存取表可以直接訪問指定的表格元素

範例:Days-in-Month Example 每月天數

當需要確定每個月份的天數,我們可以笨笨的用if-else來寫:(忽略閏年)

' ❌ 錯誤示範

If ( month = 1 ) Then
	days = 31
ElseIf ( month = 2 ) Then
	days = 28
ElseIf ( month = 3 ) Then
	days = 31
ElseIf ( month = 4 ) Then
	days = 30
ElseIf ( month = 5 ) Then
	days = 31
ElseIf ( month = 6 ) Then
	days = 30
ElseIf ( month = 7 ) Then
	days = 31
ElseIf ( month = 8 ) Then
	days = 31
ElseIf ( month = 9 ) Then
	days = 30
ElseIf ( month = 10 ) Then
	days = 31
ElseIf ( month = 11 ) Then
	days = 30
ElseIf ( month = 12 ) Then
	days = 31
End If

更簡單的方式是,將它放進array中:

**' 定義一個array**
Dim daysPerMonth() As Integer = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }

**' 使用這個array**
days = daysPerMonth( month-1 )

範例: Insurance Rates Example 保險費率

假設撰寫一個程式來計算醫療保險費率,並且費率因年齡、性別、婚姻狀況跟是否吸煙而異, 用if-else邏輯寫會得到如下結果:

// ❌ 錯誤示範

if ( gender == Gender.Female ) {
	if ( maritalStatus == MaritalStatus.Single ) {
		if ( smokingStatus == SmokingStatus.NonSmoking ) {
			if ( age < 18 ) {
				rate = 200.00;
			}
			else if ( age == 18 ) {
				rate = 250.00;
			}
			else if ( age == 19 ) {
				rate = 300.00;
			}
			...
			else if ( 65 < age ) {
				rate = 450.00;
			}
		}
		else {
			if ( age < 18 ) {
				rate = 250.00;
			}
			else if ( age == 18 ) {
				rate = 300.00;
			}
			else if ( age == 19 ) {
				rate = 350.00;
			}
			...
			else if ( 65 < age ) {
				rate = 575.00;
			}
		}
	}
	else if ( maritalStatus == MaritalStatus.Married )
	...
}

這還只是限制了其中一部分的條件,你能想像當全人類都被劃入範圍後,那個邏輯有多麼複雜嗎?

我們當然可以把年齡放進陣列中,但更好的解法是:小孩才做選擇,我 們 全 部 都 放 進 去!!

Public Enum SmokingStatus
	SmokingStatus_First = 0
	SmokingStatus_Smoking = 0
	SmokingStatus_Last = 1
	SmokingStatus_NonSmoking = 1
End Enum

Public Enum Gender
	Gender_First = 0
	Gender_Male = 0
	Gender_Last = 1
	Gender_Female = 1
End Enum
	
Public Enum MaritalStatus
	MaritalStatus_First = 0
	MaritalStatus_Single = 0
	MaritalStatus_Last = 1
	MaritalStatus_Married = 1
End Enum

Const MAX_AGE As Integer = 125

Dim rateTable ( SmokingStatus_Last, Gender_Last, MaritalStatus_Last, MAX_AGE ) As Double

表格驅動法的優點是,可以直接將其儲存成一個檔案,當執行時需要再去讀取即可。

現在我們來使用它:

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

這個方式更方便閱讀、更容易維護及修改。

範例: Flexible-Message-Format Example 動態/靈活訊息格式

在前面的範例,至少我們知道還可以用if-else來解決,但是有時當資料過於複雜時,就無法單單只用if-else去解決。

範例

假設目前你要寫一個「列印檔案中的內容」的常式,這個檔案通常有500條訊息,且分成20種分類。

這些資料的來源是某個水桶的浮標,它會將水溫存入檔案中。每條訊息會有一個ID用來辨識是哪一種的分類的訊息。

image.png

有幾個先決條件:

  1. 訊息儲存時不分先後順序,每條訊息都有ID標籤。
  2. 訊息的格式可能會變動,我們無法決定,這由業主決定。

因此我們可能會有以下的訊息資料:(除了ID以外皆每種訊息分類都可能有不同的格式)

image.png

解法1:基於邏輯的方法

image.png

假如今天又多了N種新的訊息分類,或者某個訊息改了型別,我們就得一一修改它。

以偽程式來看程式邏輯應該會是長這樣:

// 這是一段偽(代)碼 (pseudocode,詳見第9章) 
While more messages to read 
	Read a message header 
	Decode the message ID from the message header 
	If the message header is type 1 then   
		Print a type 1 message 
	Else if the message header is type 2 then   
		Print a type 2 message 
	... 
	Else if the message header is type 19 then   
		Print a type 19 message 
	Else if the message header is type 20	then   
		Print a type 20 message
End While