設計模式的C語言應用-表驅動模式
模式介紹
在傳統的23種面向對象設計模式里,并沒有表驅動這種模式。這種模式是強烈依賴數組或者多維數組的一種設計模式,不涉及類,繼承等關系,所以在C語言等非面向對象編程里得到了廣泛的應用。
表驅動是一種在C語言里常見的編程模式,從表里面查找信息而不使用邏輯語句(if和case)。核心操作是將輸入因素作為直接或者間接的索引,到數組里找到直接的結果或者對應的處理(通常是函數指針)。
表驅動實質上把邏輯和數據進行了分離。因素和結果之間的映射關系能夠全部存放到數組里,而不是混雜在if,else的流程代碼里。當映射關系發生改變的時候,只需要改變數組就可以,不需要修改代碼。管理和維護起來非常方便。甚至可以把數據作為配置文件存放到硬盤上,需要的時候讀取進來,避免了代碼重新編譯。
模式分類
根據輸入因素的不同,按照“代碼大全”一書的分類,有如下三種。
直接訪問
直接訪問是最基本最本質的一種方式。前提條件是輸入因素是整數,或者是非常簡單的一對一轉化成能作為數組元素序號。
索引訪問
輸入元素由于某些原因,難以作為數組的序號。將輸入通過函數轉化為序號,接下來和直接訪問模式一樣。
階梯訪問
輸入元素是分段的范圍,不好轉化為數組的序號。
根據輸入因素的個數,可以分為一維表查找和多維表查找。
模式實現
直接訪問模式
大部分資料拿查月份有多少天來作為例子說明。月份的數字就是自然數,月份減一恰好就可以作為數字的序號。先用這個比較俗的例子示意下。后續在舉一個實際開發中典型的例子。
普通代碼
int get_days_in_month(int mon)
{
int days;
if(1==mon){days=31;}
elseif(2==mon){days=28;}
elseif(3==mon){days=31;}
elseif(4==mon){days=30;}
elseif(5==mon){days=31;}
elseif(6==mon){days=30;}
elseif(7==mon){days=31;}
elseif(8==mon){days=31;}
elseif(9==mon){days=30;}
elseif(10==mon){days=31;}
elseif(11==mon){days=30;}
elseif(12==mon){days=31;}
return days;
}
表驅動設計的代碼
Days_in_Month[12]={31,28,31,30,31,30,31,31,30,31,30,31};
int get_days_in_month(int mon)
{
return Days_in_Month[mon-1];
}
從上可以看到,查月份有多少天這個例子屬于典型的直接訪問模式,一維表查找的例子。
在實際編程中,極少遇到這種輸入因素本身就是數字的情形。一般是把輸入的因素分類,假設為N類,然后用宏從0開始定義,以宏為序號。通常還需要設定一個MAX,來表示異常。
假設有如下場景:
某設備的工作流有幾個因素決定,(acq_cnt, zsl, cap_nzsl, small_line_buffer)。acq_cnt表示攝像頭個數,一般就1個或者2個。Zsl表示是否支持zsl,cap_nzsl是另外一個特性,small_line_buffer和設備buffer大小相關。
普通代碼如下
int get_uc(int acq_cnt, int zsl, int cap_nzsl, int small_line_buffer)
{
int df_type = 0;
switch (acq_cnt)
{
case 1:
{
if (zsl)
{
if (small_line_buffer < xxx)
{
df_type = DF_S1;
}
else
{
df_type = DF_M1;
}
}
else
{
if (cap_nzsl)
{
if (small_line_buffer < xxx)
{
df_type = DF_S3;
}
else
{
df_type = DF_M3;
}
}
else
{
if (small_line_buffer < xxx)
{
df_type = DF_S2;
}
else
{
df_type = DF_M2;
}
}
}
break;
}
case 2:
{
if (zsl && (small_line_buffer < xxx))
{
df_type = DF_D1;
}
else
{
if (cap_nzsl)
{
df_type = DF_D3;
}
else
{
df_type = DF_D2;
}
}
break;
}
default:
{
loge("acquire camera count = %d", acq_cnt);
break;
}
}
return df_type;
}
以上代碼看起來就很煩,容易出錯。如果因素改變了,還要改代碼。但是用表查找還有幾個問題。攝像頭個數acq_cnt好說,用acq_cnt-1當作需要就可以。這個方法在月份的例子用過。small_line_buffer是個麻煩,因為不可能按照buffer的byte數作為序號。不過邏輯上只需要判斷兩個狀態就可以,不如定義新的small_line_buffer為(老small_line_buffer < xxx),轉化為0,1。老small_line_buffer < xxx為1, 不滿足條件新的small_line_buffer就為0。Zsl和cap_nzsl規定為0,1狀態就可以了。
特別注意的是,這里的small_line_buffer改造,就有索引模式的意味了,因為buffer大小的數值是有很多的取值,但是small_line_buffer最后只有1個值,這就違反了直接模式里的輸入因素和索引一對一的關系。
另外兩個參數,zsl和cap_nzsl是特性,顯然原始含義也不是整數,而是各自表示兩種狀態。這就需要把狀態定義成從0開始的宏或者枚舉。我一般習慣宏。
經過改造后,表驅動如下
flow_type_e df_type_tab[2][2][2][2] =
{
[0][0][0][0] = DF_M2,
[0][0][0][1] = DF_MAX,
[0][0][1][0] = DF_M3,
[0][0][1][1] = DF_S3,
[0][1][0][0] = DF_M1,
[0][1][0][1] = DF_S1,
[0][1][1][0] = DF_MAX,
[0][1][1][1] = DF_MAX,
[1][0][0][0] = DF_MAX,
[1][0][0][1] = DF_D2,
[1][0][1][0] = DF_MAX,
[1][0][1][1] = DF_D3,
[1][1][0][0] = DF_MAX,
[1][1][0][1] = DF_D1,
[1][1][1][0] = DF_MAX,
[1][1][1][1] = DF_MAX
};
因為原邏輯中,有好幾個排列組合沒有df_type合法值,那么就定義一個DF_MAX來表示。改造完的表驅動代碼如下。
small_line_buffer = 1:0 ? line_buffer < xxx;
df_type = df_type_tab[g_msg_mgr.acq_cnt -1][zsl][cap_nzsl][small_line_buffer];
索引訪問模式
采用索引模式通常有幾個原因
1. 輸入因素取值范圍太大,而且大部分都是沒有用的,或者很多值對應的結果影響是相同的。這樣會造成數組的大量浪費,或者大到內存無法滿足。
2. 沒法量化,比如有的輸入值是浮點。
有的觀點認為,索引就一種編程形式:因為原因素取值太多,那就再建立一個查找數組表,輸入為原始因素,輸出為一個中介序號。數據密集存放在數據里,靠中介序號來訪問。這樣相當于二級直接訪問的疊加。中介表的每項只不過是一個輸出索引的大小,通常遠小于原始數據項的大小。
不過這種名詞上的爭議對編程沒有幫助。一般來講,可以把難以作為序號的輸入因素轉化為可以作為數組序號認為是一種沒有定數的方法,不必稱為一種模式。處理超大范圍的輸入有不同的方法,不一定就是靠一個中介的表驅動。如上面設備工作流例子中對small line buffer的處理。當然也可以把buffer size作為序號,放一個size大小的數組,輸出結果是0,1,但是還是太麻煩。
直接模式和索引模式在查找結果數據的方式上沒有本質不同,和階梯模式都有明顯不同。
舉個汽車管理的例子,按照重量來區分汽車類型。具體數值是否符合汽車專業的定義不影響代碼的討論。這個例子既可以用來演示索引訪問模式,又可以演示階梯訪問模式。雖然對這個例子而言最好的方式是階梯訪問模式。
0-1000kg
微型汽車
需要交費10元,綠色通道
1001kg-2000kg
小型汽車
需要交費15元,拿票
2001kg-3000kg
中型汽車
需要交費20元,停車審查
3001kg-5000kg
重型汽車
需要交費50元,走特殊寬通道,檢查輪胎。
可以看到,寫一個5000個元素的數組,每一項表示類型,和采取的動作,如交費,審查等函數操作。太浪費了。那么可以先做一個長度為5000的char數組,輸出結果只有4個值,微型,小型,中型和重型。
然后再寫一個數組,長度只有4,將汽車類型定義為宏,值從0-3。數組的內容就是交費,審查等函數指針。
#define MINI_CAR ?????????? 0
#define SMALL_CAR ??????? 1
#define MIDDLE_CAR?????? 2
#define HEAVY_CAR ???????? 3
#define MAX_CAR ??????????? 4
int weigth2type[] =
{
MINI_CAR,
MINI_CAR,
...
SMALL_CAR
MIDDLE_CAR,
MIDDLE_CAR,
...
HEAVY_CAR
};
void car_action1()
{
printf("10, express lane");
}
void car_action2()
{
printf("15, ticket");
}
void car_action3()
{
printf("20, check");
}
void car_action4()
{
printf("50, special lane, check tire");
}
void (*func[3])() =
{
car_action1,
car_action2,
car_action3,
car_action3
}
int car_type = weigth2type[wight];
(*func[car_type])();
索引的另外一個好處是,以不同的方式來生成索引,可以從不同的方面來便捷訪問數據。比如一個數組存放了姓名,年齡,體重,血壓。可以根據體重年齡等來生成索引。在嵌入式實際編碼中,這種場景用的不多。
階梯訪問模式
當輸入值是一段很大的范圍,尤其是浮點時,有些時候無法轉化為確定的索引來查表。干脆就放棄直接查表的機會,而且把范圍上下限按照次序放在表里,用for循環對比上下限的進行判斷。
階梯模式的輸入只能是范圍,這個和索引模式有所不同。索引在應付比較離散,沒有大小判斷規律的輸入時也是有效的。
還是用索引模式的汽車例子,不考慮異常情況。
int weigth2type[4]
{
1000,
2000,
3000,
5000
}
for(i = 0; i < 4; i++)
{
if(weight <= weigth2type[i])
{
(*func[i])();
}
}
經典應用
表驅動最常見在狀態機設計模式里作為狀態轉移數組,在命令設計模式里作為命令碼數組。
可以參見
設計模式的C語言應用-狀態機模式
https://bbs.huaweicloud.com/blogs/a4e37991c45811e7b8317ca23e93a891
設計模式的C語言應用-命令模式
https://bbs.huaweicloud.com/blogs/90b909e8c81711e7b8317ca23e93a891
模式總結
表驅動模式總體思路就是把邏輯關系寫到數組里,采用序號獲得結果。當代碼里出現較多的if,else或者switch的時候,會大幅增加圈復雜度,就需要考慮改寫為表驅動了。
附件: 設計模式的C語言應用-表驅動模式.pdf 636.47KB 下載次數:67次
C 語言 編程語言
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。