數據壓倒一切。如果選擇了正確的數據結構并把一切組織的井井有條,正確的算法就不言自明。編程的核心是數據結構,而不是算法。——Rob Pike
說明
本文基于這樣的認識:數據是易變的,邏輯是穩定的。 本文例舉的編程實現多為代碼片段,但不影響描述的完整性。 本文例舉的編程雖然基于C語言,但其編程思想也適用于其他語言。 此外,本文不涉及語言相關的運行效率討論。
概念提出
所謂表驅動法(Table-Driven Approach)簡而言之就是用查表的方法獲取數據。此處的“表”通常為數組,但可視為數據庫的一種體現。
根據字典中的部首檢字表查找讀音未知的漢字就是典型的表驅動法,即以每個字的字形為依據,計算出一個索引值,并映射到對應的頁數。相比一頁一頁地順序翻字典查字,部首檢字法效率極高。
具體到編程方面,在數據不多時可用邏輯判斷語句(if…else或switch…case)來獲取值;但隨著數據的增多,邏輯語句會越來越長,此時表驅動法的優勢就開始顯現。
例如,用36進制(A表示10,B表示11,…)表示更大的數字,邏輯判斷語句如下:
if(ucNum?10) { ????ucNumChar?=?ConvertToChar(ucNum); } else?if(ucNum?==?10) { ????ucNumChar?=?'A'; } else?if(ucNum?==?11) { ????ucNumChar?=?'B'; } else?if(ucNum?==?12) { ????ucNumChar?=?'C'; } //...?... else?if(ucNum?==?35) { ????ucNumChar?=?'Z'; }
當然也可以用 switch…case 結構,但實現都很冗長。而用表驅動法(將numChar 存入數組)則非常直觀和簡潔。如:
CHAR?aNumChars[]?=?{'0',?'1',?'2',?/*3~9*/'A',?'B',?'C',?/*D~Y*/'Z'}; CHAR?ucNumChar?=?aNumChars[ucNum?%?sizeof(aNumChars)];
像這樣直接將變量當作下數組下標來讀取數值的方法就是直接查表法。
注意,如果熟悉字符串操作,則上述寫法可以更簡潔:
CHAR?ucNumChar?=?"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[ucNum];
使用表驅動法時需要關注兩個問題:一是如何查表,從表中讀取正確的數據;二是表里存放什么,如數值或函數指針。前者參見1.1節“查表方式”內容,后者參見1.2節“實戰示例”內容。
查表方式
常用的查表方式有直接查找、索引查找和分段查找等。
直接查找
即直接通過數組下標獲取到數據。如果熟悉哈希表的話,可以很容易看出這種查表方式就是哈希表的直接訪問法。
如獲取星期名稱,邏輯判斷語句如下:
if(0?==?ucDay) { ????pszDayName?=?"Sunday"; } else?if(1?==?ucDay) { ????pszDayName?=?"Monday"; } //...?... else?if(6?==?ucDay) { ????pszDayName?=?"Saturday"; }
而實現同樣的功能,可將這些數據存儲到一個表里:
CHAR?*paNumChars[]?=?{"Sunday",?"Monday",?"Tuesday",?"Wednesday",?"Thursday",?"Friday",??"Saturday"}; CHAR?*pszDayName?=?paNumChars[ucDay];
類似哈希表特性,表驅動法適用于無需有序遍歷數據,且數據量大小可提前預測的情況。
對于過于復雜和龐大的判斷,可將數據存為文件,需要時加載文件初始化數組,從而在不修改程序的情況下調整里面的數值。
有時,訪問之前需要先進行一次鍵值轉換。如表驅動法表示端口忙閑時,需將槽位端口號映射為全局編號。所生成的端口數目大小的數組,其下標對應全局端口編號,元素值表示相應端口的忙閑狀態。
索引查找
有時通過一次鍵值轉換,依然無法把數據(如英文單詞等)轉為鍵值。此時可將轉換的對應關系寫到一個索引表里,即索引訪問。
如現有100件商品,4位編號,范圍從0000到9999。此時只需要申請一個長度為100的數組,且對應2位鍵值。但將4位的編號轉換為2位的鍵值,可能過于復雜或沒有規律,最合適的方法是建立一個保存該轉換關系的索引表。采用索引訪問既節省內存,又方便維護。比如索引A表示通過名稱訪問,索引B表示通過編號訪問。
分段查找
通過確定數據所處的范圍確定分類(下標)。有的數據可分成若干區間,即具有階梯性,如分數等級。此時可將每個區間的上限(或下限)存到一個表中,將對應的值存到另一表中,通過第一個表確定所處的區段,再由區段下標在第二個表里讀取相應數值。注意要留意端點,可用二分法查找,另外可考慮通過索引方法來代替。
如根據分數查績效等級:
#define?MAX_GRADE_LEVEL???(INT8U)5 DOUBLE?aRangeLimit[MAX_GRADE_LEVEL]?=?{50.0,?60.0,?70.0,?80.0,?100.0}; CHAR?*paGrades[MAX_GRADE_LEVEL]?=?{"Fail",?"Pass",?"Credit",?"Distinction",?"High?Distinction"}; static?CHAR*?EvaluateGrade(DOUBLE?dScore) { ????INT8U?ucLevel?=?0; ????for(;?ucLevel?上述兩張表(數組)也可合并為一張表(結構體數組),如下所示:
typedef?struct{ ????DOUBLE??aRangeLimit; ????CHAR????*pszGrade; }T_GRADE_MAP; T_GRADE_MAP?gGradeMap[MAX_GRADE_LEVEL]?=?{ ????{50.0,??????????????"Fail"}, ????{60.0,??????????????"Pass"}, ????{70.0,??????????????"Credit"}, ????{80.0,??????????????"Distinction"}, ????{100.0,?????????????"High?Distinction"} }; static?CHAR*?EvaluateGrade(DOUBLE?dScore) { ????INT8U?ucLevel?=?0; ????for(;?ucLevel?該表結構已具備的數據庫的雛形,并可擴展支持更為復雜的數據。其查表方式通常為索引查找,偶爾也為分段查找;當索引具有規律性(如連續整數)時,退化為直接查找。
使用分段查找法時應注意邊界,將每一分段范圍的上界值都考慮在內。找出所有不在最高一級范圍內的值,然后把剩下的值全部歸入最高一級中。有時需要人為地為最高一級范圍添加一個上界。
同時應小心不要錯誤地用“<”來代替“<=”。要保證循環在找出屬于最高一級范圍內的值后恰當地結束,同時也要保證恰當處理范圍邊界。
實戰示例
本節多數示例取自實際項目。表形式為一維數組、二維數組和結構體數組;表內容有數據、字符串和函數指針。基于表驅動的思想,表形式和表內容可衍生出豐富的組合。
字符統計
問題:統計用戶輸入的一串數字中每個數字出現的次數。
普通解法主體代碼如下:
INT32U?aDigitCharNum[10]?=?{0};?/*?輸入字符串中各數字字符出現的次數?*/ INT32U?dwStrLen?=?strlen(szDigits); INT32U?dwStrIdx?=?0; for(;?dwStrIdx?這種解法的缺點顯而易見,既不美觀也不靈活。其問題關鍵在于未將數字字符與數組aDigitCharNum下標直接關聯起來。
以下示出更簡潔的實現方式:
for(;?dwStrIdx?上述實現考慮到0也為數字字符。該解法也可擴展至統計所有ASCII可見字符。
月天校驗
問題:對給定年份和月份的天數進行校驗(需區分平年和閏年)。
普通解法主體代碼如下:
switch(OnuTime.Month)
{ ????case?1: ????case?3: ????case?5: ????case?7: ????case?8: ????case?10: ????case?12: ????????if(OnuTime.Day>31?||?OnuTime.Day<1) ????????{ ????????????CtcOamLog(FUNCTION_Pon,"Don't?support?this?Day:?%d(1~31)!!! ",?OnuTime.Day); ????????????retcode?=?S_ERROR; ????????} ????????break; ????case?2: ????????if(((OnuTime.Year%4?==?0)?&&?(OnuTime.Year%100?!=?0))?||?(OnuTime.Year%400?==?0)) ????????{ ????????????if(OnuTime.Day>29?||?OnuTime.Day<1) ????????????{ ????????????????CtcOamLog(FUNCTION_Pon,"Don't?support?this?Day:?%d(1~29)!!! ",?OnuTime.Day); ????????????????retcode?=?S_ERROR; ????????????} ????????} ????????else ????????{ ????????????if(OnuTime.Day>28?||?OnuTime.Day<1) ????????????{ ????????????????CtcOamLog(FUNCTION_Pon,"Don't?support?this?Day:?%d(1~28)!!! ",?OnuTime.Day); ????????????????retcode?=?S_ERROR; ????????????} ????????} ????????break; ????case?4: ????case?6: ????case?9: ????case?11: ????????if(OnuTime.Day>30?||?OnuTime.Day<1) ????????{ ????????????CtcOamLog(FUNCTION_Pon,"Don't?support?this?Day:?%d(1~30)!!! ",?OnuTime.Day); ????????????retcode?=?S_ERROR; ????????} ????????break; ????default: ????????CtcOamLog(FUNCTION_Pon,"Don't?support?this?Month:?%d(1~12)!!! ",?OnuTime.Month); ????????retcode?=?S_ERROR; ????????break; }以下示出更簡潔的實現方式:
#define?MONTH_OF_YEAR?12????/*?一年中的月份數?*/ /*?閏年:能被4整除且不能被100整除,或能被400整除?*/ #define?IS_LEAP_YEAR(year)?((((year)?%?4?==?0)?&&?((year)?%?100?!=?0))?||?((year)?%?400?==?0)) /*?平年中的各月天數,下標對應月份?*/ INT8U?aDayOfCommonMonth[MONTH_OF_YEAR]?=?{31,?28,?31,?30,?31,?30,?31,?31,?30,?31,?30,?31}; INT8U?ucMaxDay?=?0; if((OnuTime.Month?==?2)?&&?(IS_LEAP_YEAR(OnuTime.Year))) ????ucMaxDay?=?aDayOfCommonMonth[1]?+?1; else ????ucMaxDay?=?aDayOfCommonMonth[OnuTime.Month-1]; if((OnuTime.Day?1)?||?(OnuTime.Day?>?ucMaxDay) { ????CtcOamLog(FUNCTION_Pon,"Month?%d?doesn't?have?this?Day:?%d(1~%d)!!! ", ??????????????OnuTime.Month,?OnuTime.Day,?ucMaxDay); ????retcode?=?S_ERROR; }名稱構造
問題:根據WAN接口承載的業務類型(Bitmap)構造業務類型名稱字符串。
普通解法主體代碼如下:
void?Sub_SetServerType(INT8U?*ServerType,?INT16U?wan_servertype) { ????if?((wan_servertype?&?0x0001)?==?0x0001) ????{ ????????strcat(ServerType,?"_INTERNET"); ????} ????if?((wan_servertype?&?0x0002)?==?0x0002) ????{ ????????strcat(ServerType,?"_TR069"); ????} ????if?((wan_servertype?&?0x0004)?==?0x0004) ????{ ????????strcat(ServerType,?"_VOIP"); ????} ????if?((wan_servertype?&?0x0008)?==?0x0008) ????{ ????????strcat(ServerType,?"_OTHER"); ????} }以下示出C語言中更簡潔的實現方式:
/*?獲取var變量第bit位,編號從右至左?*/ #define??GET_BIT(var,?bit)???(((var)?>>?(bit))?&?0x1) const?CHAR*?paSvrNames[]?=?{"_INTERNET",?"_TR069",?"_VOIP",?"_OTHER"}; const?INT8U?ucSvrNameNum?=?sizeof(paSvrNames)?/?sizeof(paSvrNames[0]); VOID?SetServerType(CHAR?*pszSvrType,?INT16U?wSvrType) { ????INT8U?ucIdx?=?0; ????for(;?ucIdx?新的實現將數據和邏輯分離,維護起來非常方便。只要邏輯(規則)不變,則唯一可能的改動就是數據(paSvrNames)。
值名解析
問題:根據枚舉變量取值輸出其對應的字符串,如PORT_FE(1)輸出“Fe”。
//值名映射表結構體定義,用于數值解析器 typedef?struct{ ????INT32U?dwElem;????//待解析數值,通常為枚舉變量 ????CHAR*??pszName;???//指向數值所對應解析名字符串的指針 }T_NAME_PARSER; /****************************************************************************** *?函數名稱:??NameParser *?功能說明:??數值解析器,將給定數值轉換為對應的具名字符串 *?輸入參數:??VOID?*pvMap???????:值名映射表數組,含T_NAME_PARSER結構體類型元素 ????????????????????????????????VOID指針允許用戶在保持成員數目和類型不變的前提下, ????????????????????????????????定制更有意義的結構體名和/或成員名。 ?????????????INT32U?dwEntryNum?:值名映射表數組條目數 ?????????????INT32U?dwElem?????:待解析數值,通常為枚舉變量 ?????????????INT8U*?pszDefName?:缺省具名字符串指針,可為空 *?輸出參數:??NA *?返回值??:??INT8U?*:?數值所對應的具名字符串 ?????????????當無法解析給定數值時,若pszDefName為空,則返回數值對應的16進制格式 ?????????????字符串;否則返回pszDefName。 ******************************************************************************/ INT8U?*NameParser(VOID?*pvMap,?INT32U?dwEntryNum,?INT32U?dwElem,?INT8U*?pszDefName) { ????CHECK_SINGLE_POINTER(pvMap,?"NullPoniter"); ????INT32U?dwEntryIdx?=?0; ????for(dwEntryIdx?=?0;?dwEntryIdx?dwElem) ????????{ ????????????return?ptNameParser->pszName; ????????} ????????//ANSI標準禁止對void指針進行算法操作;GNU標準則指定void*算法操作與char*一致。 ????????//若考慮移植性,可將pvMap類型改為INT8U*,或定義INT8U*局部變量指向pvMap。 ????????pvMap?+=?sizeof(T_NAME_PARSER); ????} ????if(NULL?!=?pszDefName) ????{ ????????return?pszDefName; ????} ????else ????{ ????????static?INT8U?szName[12]?=?{0};?//Max:"0xFFFFFFFF" ????????sprintf(szName,?"0x%X",?dwElem); ????????return?szName; ????} }以下給出NameParser的簡單應用示例:
//UNI端口類型值名映射表結構體定義 typedef?struct{ ????INT32U?dwPortType; ????INT8U*?pszPortName; }T_PORT_NAME; //UNI端口類型解析器 T_PORT_NAME?gUniNameMap[]?=?{ ????{1,??????"Fe"}, ????{3,??????"Pots"}, ????{99,?????"Vuni"} }; const?INT32U?UNI_NAM_MAP_NUM?=?(INT32U)(sizeof(gUniNameMap)/sizeof(T_PORT_NAME)); VOID?NameParserTest(VOID) { ????INT8U?ucTestIndex?=?1; ????printf("[%s]?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("Unknown",?NameParser(gUniNameMap,?UNI_NAM_MAP_NUM,?0,?"Unknown"))???"ERROR"?:?"OK"); ????printf("[%s] ?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("DefName",?NameParser(gUniNameMap,?UNI_NAM_MAP_NUM,?0,?"DefName"))???"ERROR"?:?"OK"); ????printf("[%s] ?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("Fe",?NameParser(gUniNameMap,?UNI_NAM_MAP_NUM,?1,?"Unknown"))???"ERROR"?:?"OK"); ????printf("[%s] ?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("Pots",?NameParser(gUniNameMap,?UNI_NAM_MAP_NUM,?3,?"Unknown"))???"ERROR"?:?"OK"); ????printf("[%s] ?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("Vuni",?NameParser(gUniNameMap,?UNI_NAM_MAP_NUM,?99,?NULL))???"ERROR"?:?"OK"); ????printf("[%s] ?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("Unknown",?NameParser(gUniNameMap,?UNI_NAM_MAP_NUM,?255,?"Unknown"))???"ERROR"?:?"OK"); ????printf("[%s] ?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("0xABCD",?NameParser(gUniNameMap,?UNI_NAM_MAP_NUM,?0xABCD,?NULL))???"ERROR"?:?"OK"); ????printf("[%s] ?Result:?%s! ",?__FUNCTION__,?ucTestIndex++, ???????????strcmp("NullPoniter",?NameParser(NULL,?UNI_NAM_MAP_NUM,?0xABCD,?NULL))???"ERROR"?:?"OK"); } gUniNameMap在實際項目中有十余個條目,若采用邏輯鏈實現將非常冗長。
取值映射
問題:不同模塊間同一參數枚舉值取值可能有所差異,需要適配。
此處不再給出普通的switch…case或if…else if…else結構,而直接示出以下表驅動實現:
typedef?struct{ ????PORTSTATE?loopMEState; ????PORTSTATE?loopMIBState; }LOOPMAPSTRUCT; static?LOOPMAPSTRUCT?s_CesLoop[]?=?{ ????{NO_LOOP,??????????????????e_ds1_looptype_noloop}, ????{PAYLOAD_LOOP,?????????????e_ds1_looptype_PayloadLoop}, ????{LINE_LOOP,????????????????e_ds1_looptype_LineLoop}, ????{PON_LOOP,?????????????????e_ds1_looptype_OtherLoop}, ????{CES_LOOP,?????????????????e_ds1_looptype_InwardLoop}}; PORTSTATE?ConvertLoopMEStateToMIBState(PORTSTATE?vPortState) { ????INT32U?num?=?0,?ii; ???? ????num?=?ARRAY_NUM(s_CesLoop); ????for(ii?=?0;?ii? 相應地,從loopMIBState映射到loopMEState需要定義一個ConvertLoopMIBStateToMEState函數。更進一步,所有類似的一對一映射關系都必須如上的映射(轉換)函數,相當繁瑣。 事實上,從抽象層面看,該映射關系非常簡單。提取共性后定義帶參數宏,如下所示:/********************************************************** *?功能描述:進行二維數組映射表的一對一映射,用于參數適配 *?參數說明:map ???????--?二維數組映射表 ????????????elemSrc????--?映射源,即待映射的元素值 ????????????elemDest???--?映射源對應的映射結果 ??????????? direction ?--?映射方向字節,表示從數組哪列映射至哪列。 ??????????????????????????高4位對應映射源列,低4位對應映射結果列。 ????????????defaultVal?--?映射失敗時置映射結果為缺省值 *?示例:ARRAY_MAPPER(gCesLoopMap, 3, ucLoop, 0x10, NO_LOOP); ????????????則ucLoop?=?2(LINE_LOOP) **********************************************************/ #define?ARRAY_MAPPER(map,?elemSrc,?elemDest,?direction,?defaultVal)?do{ ????INT8U?ucMapIdx?=?0,?ucMapNum?=?0;? ????ucMapNum?=?sizeof(map)/sizeof(map[0]);? ????for(ucMapIdx?=?0;?ucMapIdx?>4])? ????????{? ????????????elemDest?=?map[ucMapIdx][(direction)&0x0F];? ????????????break;? ????????}? ????}? ????if(ucMapIdx?==?ucMapNum)? ????{? ????????elemDest?=?(defaultVal);? ????}? }while(0)參數取值轉換時直接調用統一的映射器宏,如下:
static?INT8U?gCesLoopMap[][2]?=?{ ????{NO_LOOP,??????????????????e_ds1_looptype_noloop}, ????{PAYLOAD_LOOP,?????????????e_ds1_looptype_PayloadLoop}, ????{LINE_LOOP,????????????????e_ds1_looptype_LineLoop}, ????{PON_LOOP,?????????????????e_ds1_looptype_OtherLoop}, ????{CES_LOOP,?????????????????e_ds1_looptype_InwardLoop}}; ARRAY_MAPPER(gCesLoopMap,?tPara.dwParaVal[0],?dwLoopConf,?0x01,?e_ds1_looptype_noloop);另舉一例:
#define??CES_DEFAULT_JITTERBUF????????(INT32U)2000???/*?默認jitterbuf為2000us,而1幀=125us?*/ #define??CES_JITTERBUF_STEP???????????(INT32U)125????/*?jitterbuf步長為125us,即1幀?*/ #define??CES_DEFAULT_QUEUESIZE????????(INT32U)5 #define??CES_DEFAULT_MAX_QUEUESIZE????(INT32U)7 #define??ARRAY_NUM(array)?????????????(sizeof(array)?/?sizeof((array)[0]))??/*?數組元素個數?*/ typedef?struct{ ????INT32U??dwJitterBuffer; ????INT32U??dwFramePerPkt; ????INT32U??dwQueueSize; }QUEUE_SIZE_MAP; /*?gCesQueueSizeMap也可以(JitterBuffer?/?FramePerPkt)值為索引,更加緊湊?*/ static?QUEUE_SIZE_MAP?gCesQueueSizeMap[]=?{ ???????{1,1,1},??{1,2,1},??{2,1,2},??{2,2,1}, ???????{3,1,3},??{3,2,1},??{4,1,3},??{4,2,1}, ???????{5,1,4},??{5,2,3},??{6,1,4},??{6,2,3}, ???????{7,1,4},??{7,2,3},??{8,1,4},??{8,2,3}, ???????{9,1,5},??{9,2,4},??{10,1,5},?{10,2,4}, ???????{11,1,5},?{11,2,4},?{12,1,5},?{12,2,4}, ???????{13,1,5},?{13,2,4},?{14,1,5},?{14,2,4}, ???????{15,1,5},?{15,2,4},?{16,1,5},?{16,2,4}, ???????{17,1,6},?{17,2,5},?{18,1,6},?{18,2,5}, ???????{19,1,6},?{19,2,5},?{20,1,6},?{20,2,5}, ???????{21,1,6},?{21,2,5},?{22,1,6},?{22,2,5}, ???????{23,1,6},?{23,2,5},?{24,1,6},?{24,2,5}, ???????{25,1,6},?{25,2,5},?{26,1,6},?{26,2,5}, ???????{27,1,6},?{27,2,5},?{28,1,6},?{28,2,5}, ???????{29,1,6},?{29,2,5},?{30,1,6},?{30,2,5}, ???????{31,1,6},?{31,2,5},?{32,1,6},?{32,2,5}}; /********************************************************** *?函數名稱:CalcQueueSize *?功能描述:根據JitterBuffer和FramePerPkt計算QueueSize *?注意事項:配置的最大緩存深度 *????????????=?2?*?JitterBuffer?/?FramePerPkt *????????????=?2?*?N?Packet?=?2?^?QueueSize *????????????JitterBuffer為125us幀速率的倍數, *????????????FramePerPkt為每個分組的幀數, *??????????? QueueSize向上取整,最大為7。 **********************************************************/ INT32U?CalcQueueSize(INT32U?dwJitterBuffer,?INT32U?dwFramePerPkt) { ????INT8U?ucIdx?=?0,?ucNum?=?0; ????//本函數暫時僅考慮E1 ????ucNum?=?ARRAY_NUM(gCesQueueSizeMap); ????for(ucIdx?=?0;?ucIdx?版本控制
問題:控制OLT與ONU之間的版本協商。ONU本地設置三比特控制字,其中bit2(MSB)~bit0(LSB)分別對應0x21、0x30和0xAA版本號;且bitX為0表示上報對應版本號,bitX為1表示不上報對應版本號。其他版本號如0x20、0x13和0x1必須上報,即不受控制。
最初的實現采用if…else if…else結構,代碼非常冗長,如下:
pstSendTlv->ucLength?=?0x1f; if?(gOamCtrlCode?==?0) { ????vosMemCpy(pstSendTlv->aucVersionList,?ctc_oui,?3); ????pstSendTlv->aucVersionList[3]?=?0x30; ????vosMemCpy(&(pstSendTlv->aucVersionList[4]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[7]?=?0x21; ????vosMemCpy(&(pstSendTlv->aucVersionList[8]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[11]?=?0x20; ????vosMemCpy(&(pstSendTlv->aucVersionList[12]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[15]?=?0x13; ????vosMemCpy(&(pstSendTlv->aucVersionList[16]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[19]?=?0x01; ????vosMemCpy(&(pstSendTlv->aucVersionList[20]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[23]?=?0xaa; } else?if?(gOamCtrlCode?==?1) { ????vosMemCpy(pstSendTlv->aucVersionList,?ctc_oui,?3); ????pstSendTlv->aucVersionList[3]?=?0x30; ????vosMemCpy(&(pstSendTlv->aucVersionList[4]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[7]?=?0x21; ????vosMemCpy(&(pstSendTlv->aucVersionList[8]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[11]?=?0x20; ????vosMemCpy(&(pstSendTlv->aucVersionList[12]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[15]?=?0x13; ????vosMemCpy(&(pstSendTlv->aucVersionList[16]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[19]?=?0x01; } //此處省略gOamCtrlCode?==?2~6的處理代碼 else?if?(gOamCtrlCode?==?7) { ????vosMemCpy(&(pstSendTlv->aucVersionList),?ctc_oui,?3); ????pstSendTlv->aucVersionList[3]?=?0x20; ????vosMemCpy(&(pstSendTlv->aucVersionList[4]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[7]?=?0x13; ????vosMemCpy(&(pstSendTlv->aucVersionList[8]),?ctc_oui,?3); ????pstSendTlv->aucVersionList[11]?=?0x01; }以下示出C語言中更簡潔的實現方式(基于二維數組):
/********************************************************************** *?版本控制字數組定義 * gOamCtrlCode:?? Bitmap控制字。Bit-X為0時上報對應版本,Bit-X為1時屏蔽對應版本。 * CTRL_VERS_NUM:??可控版本個數。 * CTRL_CODE_NUM:??控制字個數。與CTRL_VERS_NUM有關。 * gOamVerCtrlMap:?版本控制字數組。行對應控制字,列對應可控版本。 ??????????????????元素值為0時不上報對應版本,元素值非0時上報該元素值。 * Note:?該數組旨在實現“數據與控制隔離”。后續若要新增可控版本,只需修改 ??????????????????--?CTRL_VERS_NUM ??????????????????--?gOamVerCtrlMap新增行(控制字) ??????????????????--?gOamVerCtrlMap新增列(可控版本) **********************************************************************/ #define?CTRL_VERS_NUM????3 #define?CTRL_CODE_NUM????(1<aucVersionList[index],?ctc_oui,?3); ????????index?+=?3; ????????pstSendTlv->aucVersionList[index++]?=?gOamVerCtrlMap[gOamCtrlCode][verIdx]; ????} } vosMemCpy(&pstSendTlv->aucVersionList[index],?ctc_oui,?3); index?+=?3; pstSendTlv->aucVersionList[index++]?=?0x20; vosMemCpy(&pstSendTlv->aucVersionList[index],?ctc_oui,?3); index?+=?3; pstSendTlv->aucVersionList[index++]?=?0x13; vosMemCpy(&pstSendTlv->aucVersionList[index],?ctc_oui,?3); index?+=?3; pstSendTlv->aucVersionList[index++]?=?0x01; pstSendTlv->ucLength?=?INFO_TYPE_VERS_LEN?+?index; 消息處理
問題:終端輸入不同的打印命令,調用相應的打印函數,以控制不同級別的打印。
這是一段消息(事件)驅動程序。本模塊接收其他模塊(如串口驅動)發送的消息,根據消息中的打印級別字符串和開關模式,調用不同函數進行處理。常見的實現方法如下:
void?logall(void) { ????g_log_control[0]?=?0xFFFFFFFF; } void?noanylog(void) { ????g_log_control[0]?=?0; } void?logOam(void) { ????g_log_control[0]?|=?(0x01?<以下示出C語言中更簡潔的實現方式:
typedef?struct{ ????OAM_LOG_OFF?=?(INT8U)0, ????OAM_LOG_ON??=?(INT8U)1 }E_OAM_LOG_MODE; typedef?FUNC_STATUS?(*OamLogHandler)(VOID); typedef?struct{ ????CHAR???????????*pszLogCls;????/*?打印級別?*/ ????E_OAM_LOG_MODE?eLogMode;??????/*?打印模式?*/ ????OamLogHandler??fnLogHandler;??/*?打印函數?*/ }T_OAM_LOG_MAP; T_OAM_LOG_MAP?gOamLogMap[]?=?{ ????{"all",?????????OAM_LOG_OFF,???????noanylog}, ????{"oam",?????????OAM_LOG_OFF,???????nologOam}, ????//...?... ????{"version",?????OAM_LOG_OFF,???????nologVersion}, ???? ????{"all",?????????OAM_LOG_ON,????????logall}, ????{"oam",?????????OAM_LOG_ON,????????logOam}, ????//...?... ????{"version",?????OAM_LOG_ON,????????logVersion} }; INT32U?gOamLogMapNum?=?sizeof(gOamLogMap)?/?sizeof(T_OAM_LOG_MAP); VOID?logExec(CHAR?*pszName,?INT8U?ucSwitch) { ????INT8U?ucIdx?=?0; ????for(;?ucIdx? 這種表驅動消息處理實現的優點如下:
1.增強可讀性,消息如何處理從表中一目了然。2.增強可擴展性。更容易修改,要增加新的消息,只要修改數據即可,不需要修改流程。
3.降低復雜度。通過把程序邏輯的復雜度轉移到人類更容易處理的數據中來,從而達到控制復雜度的目標。
4.主干清晰,代碼重用。
若各索引為順序枚舉值,則建立多維數組(每維對應一個索引),根據下標直接定位到處理函數,效率會更高。
注意,考慮到本節實例中logOam/logPon或nologOam/nologPon等函數本質上是基于打印級別的比特操作,因此可進一步簡化。以下例舉其相似實現:
/*?日志控制類型定義?*/ typedef?enum { ????LOG_NORM?=?0,????????/*?未分類日志,可用于通用日志?*/ ????LOG_FRM,?????????????/*?Frame,OMCI幀日志?*/ ????LOG_PON,?????????????/*?Pon,光鏈路相關日志?*/ ????LOG_ETH,?????????????/*?Ethernet,Layer2以太網日志?*/ ????LOG_NET,?????????????/*?Internet,Layer3網絡日志?*/ ????LOG_MULT,????????????/*?Multicast,組播日志?*/ ????LOG_QOS,?????????????/*?QOS,流量日志?*/ ????LOG_CES,?????????????/*?Ces,TDM電路仿真日志?*/ ????LOG_VOIP,????????????/*?Voip,語音日志?*/ ????LOG_ALM,?????????????/*?Alarm,告警日志?*/ ????LOG_PERF,????????????/*?Performance,性能統計日志?*/ ????LOG_VER,?????????????/*?Version,軟件升級日志?*/ ????LOG_XDSL,????????????/*?xDsl日志?*/ ????LOG_DB,??????????????/*?數據庫操作日志?*/ ????//新日志類型在此處擴展,共支持32種日志類型 ????LOG_ALL?=?UINT_MAX???/*?所有日志類型?*/ }E_LOG_TYPE; /***************************************************************************** ?*?變量名稱:gOmciLogCtrl ?*?作用描述:OMCI日志控制字,BitMap格式(比特編號從LSB至MSB依次為Bit0->BitN)。 ?*?????????? Bit0~N分別對應E_LOG_TYPE各枚舉值(除LOG_ALL外)。 ?*?????????? BitX為0時關閉日志類型對應的日志功能,BitX為1時則予以打開。 ?*?變量范圍:該變量為四字節整型靜態全局變量,即支持32種日志類型。 ?*?訪問說明:通過GetOmciLogCtrl/SetOmciLogCtrl/OmciLogCtrl函數訪問/設置控制字。 ?*****************************************************************************/ static?INT32U?gOmciLogCtrl?=?0; //日志類型字符串數組,下標為各字符串所對應的日志類型枚舉值。 static?const?INT8U*?paLogTypeName[]?=?{ ????"Norm",????????"Frame",???"Pon",??"Ethernet",??"Internet", ????"Multicast",???"Qos",?????"Ces",??"Voip",??????"Alarm", ????"Performance",?"Version",?"Xdsl",??"Db" }; static?const?INT8U??ucLogTypeNameNum?=?sizeof(paLogTypeName)?/?sizeof(paLogTypeName[0]); static?VOID?SetGlobalLogCtrl(E_LOG_TYPE?eLogType,?INT8U?ucLogSwitch) { ????if(LOG_ON?==?ucLogSwitch) ????????gOmciLogCtrl?=?LOG_ALL; ????else ????????gOmciLogCtrl?=?0; } static?VOID?SetSpecificLogCtrl(E_LOG_TYPE?eLogType,?INT8U?ucLogSwitch) { ????if(LOG_ON?==?ucLogSwitch) ????????SET_BIT(gOmciLogCtrl,?eLogType); ????else ????????CLR_BIT(gOmciLogCtrl,?eLogType); } VOID?OmciLogCtrl(CHAR?*pszLogType,?INT8U?ucLogSwitch) { ????if(0?==?strncasecmp(pszLogType,?"All",?LOG_TYPE_CMP_LEN)) ????{ ????????SetGlobalLogCtrl(LOG_ALL,?ucLogSwitch); ????????return; ????} ????INT8U?ucIdx?=?0; ????for(ucIdx?=?0;?ucIdx?編程思想
表驅動法屬于數據驅動編程的一種,其核心思想在《Unix編程藝術》和《代碼大全2》中均有闡述。兩者均認為人類閱讀復雜數據結構遠比復雜的控制流程容易,即相對于程序邏輯,人類更擅長于處理數據。
本節將由Unix設計原則中的“分離原則”和“表示原則”展開。
分離原則:策略同機制分離,接口同引擎分離
機制即提供的功能;策略即如何使用功能。
策略的變化要遠遠快于機制的變化。將兩者分離,可以使機制相對保持穩定,而同時支持策略的變化。
代碼大全中提到“隔離變化”的概念,以及設計模式中提到的將易變化的部分和不易變化的部分分離也是這個思路。
表示原則:把知識疊入數據以求邏輯質樸而健壯
即使最簡單的程序邏輯讓人類來驗證也很困難,但就算是很復雜的數據,對人類來說,還是相對容易推導和建模的。數據比編程邏輯更容易駕馭。在復雜數據和復雜代碼中選擇,寧可選擇前者。更進一步,在設計中,應該主動將代碼的復雜度轉移到數據中去(參考“版本控制”)。
在“消息處理”示例中,每個消息處理的邏輯不變,但消息可能是變化的。將容易變化的消息和不容易變化的查找邏輯分離,即“隔離變化”。此外,該例也體現消息內部的處理邏輯(機制)和不同的消息處理(策略)分離。
數據驅動編程可以應用于:
1.函數級設計,如本文示例。
2.程序級設計,如用表驅動法實現狀態機。
3.系統級設計,如DSL。
注意,數據驅動編程不是全新的編程模型,只是一種設計思路,在Unix/Linux開源社區應用很多。數據驅動編程中,數據不但表示某個對象的狀態,實際上還定義程序的流程,這點不同于面向對象設計中的數據“封裝”。
附錄
網友觀點
(以下觀點摘自博客園網友“七心葵”的回帖,非常具有啟發性。)
Booch的《面向對象分析與設計》一書中,提到所有的程序設計語言大概有3個源流:結構化編程;面向對象編程;數據驅動編程。
我認為數據驅動編程的本質是“參數化抽象”的思想,不同于OO的“規范化抽象”的思想。
數據驅動編程在網絡游戲開發過程中很常用,但是少有人專門提到這個詞。
數據驅動編程有很多名字:元編程,解釋器/虛擬機,LOP/微語言/DSL等。包括聲明式編程、標記語言、甚至所見即所得的拖放控件,都算是數據驅動編程的一種吧。
數據驅動編程可以幫助處理復雜性,和結構化編程、OO 均可相容。(正交的角度)
將變和不變的部分分離,策略和機制分離,由此聯想到的還有:(數據和代碼的分離,微語言和解釋器的分離,被生成代碼和代碼生成器的分離);更近一步:(微內核插件式體系結構)
元編程應該說是更加泛化的數據驅動編程,元編程不是新加入一個間接層,而是退居一步,使得當前的層變成一個間接層。元編程分為靜態元編程(編譯時)和動態元編程(運行時),靜態元編程本質上是一種 代碼生成技術或者編譯器技術;動態元編程一般通過解釋器(或虛擬機)加以實現。
數據驅動編程當然也不應該說是“反抽象的”,但的確與“OO抽象”的思維方式是迥然不同,涇渭分明的,如TAOUP一書中所述:“在Unix的模塊化傳統和圍繞OO語言發展起來的使用模式之間,存在著緊張的對立關系”應該說數據驅動編程的思路與結構化編程和OO是正交的,更類似一種“跳出三界外,不在五行中”的做法。
編程和人的關系
人類心智的限制,一切的背后都有人的因素作為依據:
a、 人同時關注的信息數量:7+-2 (所以要分模塊) b、 人接收一組新信息的平均時間5s (所以要簡單,系統總的模塊數不要太多) c、人思維的直觀性(人的視覺能力和模糊思維能力),這意味這兩點:
A “直”——更善于思考自己能直接接觸把玩的東西;(所以要“淺平透”、使用具象的設計,要盡量代碼中只有順直的流程),
B “觀”——更善于觀圖而不是推算邏輯;(所以要表驅動法,數據驅動編程,要UML,要可視化編程——當然MDA是太理想化了)
d、 人不能持續集中注意力(人在一定的代碼行數中產生的bug數量的比例是一定的,所以語言有具有表現力,要體現表達的經濟性)
所以要機制與策略分離,要數據和代碼分離(數據驅動編程),要微語言,要DSL,要LOP……
e、 人是有創造欲,有現實利益心的(只要偶可能總是不夠遵從規范,或想創造規范謀利——只要成本能承受,在硬件領域就不行)
另外,開一個有意思的玩笑,Unix編程藝術藝術的英文縮寫為TAOUP,我覺得可以理解為UP之TAO——向上拋出之道——將復雜的易變的邏輯作為數據或更高層代碼拋給上層!
函數指針
“消息處理”一節示例中的函數指針有點插件結構的味道。可對這些插件進行方便替換,新增,刪除,從而改變程序的行為。而這種改變,對事件處理函數的查找又是隔離的(隔離變化)。
函數指針非常有用,但使用時需注意其缺陷:無法檢查參數(parameter)和返回值(return value)的類型。因為函數已經退化成指針,而指針不攜帶這些類型信息。缺少類型檢查,當參數或返回值不一致時,可能會造成嚴重的錯誤。
例如,定義三個函數,分別具有兩個參數:
int?max(int?x,?int?y)??{??return?x>y?x:y;??} int?min(int?x,?int?y)??{??return?x
而處理函數卻定義為:
int?process(int?x,?int?y,?int?(*f)())??{??return?(*f)(x,?y);??}其中,第三個參數是一個沒有參數且返回int型變量的函數指針。但后面卻用process(a,b,max)的方式進行調用,max帶有兩個參數。若編譯器未檢查出錯誤,而又不小心將return (*f)(x,y);寫成return (*f)(x);,那么后果可能很嚴重。因此在C語言中使用函數指針時,一定要小心"類型陷阱"。
編輯:黃飛
?
評論