C語言表驅動法編程實踐

 

數據壓倒一切。若是選擇了正確的數據結構並把一切組織的層次分明,正確的算法就不言自明。編程的核心是數據結構,而不是算法。git

——Rob Pike 算法

說明

     本文基於這樣的認識:數據是易變的,邏輯是穩定的。數據庫

     本文例舉的編程實現多爲代碼片斷,但不影響描述的完整性。編程

     本文例舉的編程雖然基於C語言,但其編程思想也適用於其餘語言。設計模式

     此外,本文不涉及語言相關的運行效率討論。數組

 

 

1 概念提出

     所謂表驅動法(Table-Driven Approach)簡而言之就是用查表的方法獲取數據。此處的「表」一般爲數組,但可視爲數據庫的一種體現。緩存

     根據字典中的部首檢字表查找讀音未知的漢字就是典型的表驅動法,即以每一個字的字形爲依據,計算出一個索引值,並映射到對應的頁數。相比一頁一頁地順序翻字典查字,部首檢字法效率極高。網絡

     具體到編程方面,在數據很少時可用邏輯判斷語句(if…else或switch…case)來獲取值;但隨着數據的增多,邏輯語句會愈來愈長,此時表驅動法的優點就開始顯現。數據結構

 

     例如,用36進制(A表示10,B表示11,…)表示更大的數字,邏輯判斷語句以下:ide

 1 if(ucNum < 10)
 2 {
 3     ucNumChar = ConvertToChar(ucNum);
 4 }
 5 else if(ucNum == 10)
 6 {
 7     ucNumChar = 'A';
 8 }
 9 else if(ucNum == 11)
10 {
11     ucNumChar = 'B';
12 }
13 else if(ucNum == 12)
14 {
15     ucNumChar = 'C';
16 }
17 //... ...
18 else if(ucNum == 35)
19 {
20     ucNumChar = 'Z';
21 }
View Code

     固然也能夠用switch…case結構,但實現都很冗長。而用表驅動法(將numChar存入數組)則很是直觀和簡潔。如:

1 CHAR aNumChars[] = {'0', '1', '2', /*3~9*/'A', 'B', 'C', /*D~Y*/'Z'};
2 CHAR ucNumChar = aNumChars[ucNum % sizeof(aNumChars)];
View Code

     像這樣直接將變量看成下數組下標來讀取數值的方法就是直接查表法。

     注意,若是熟悉字符串操做,則上述寫法能夠更簡潔:

1 CHAR ucNumChar = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[ucNum];
View Code

 

     使用表驅動法時須要關注兩個問題:一是如何查表,從表中讀取正確的數據;二是表裏存放什麼,如數值或函數指針。前者參見1.1節「查表方式」內容,後者參見1.2節「實戰示例」內容。

 

1.1 查表方式

     經常使用的查表方式有直接查找、索引查找和分段查找等。

1.1.1 直接查找

     即直接經過數組下標獲取到數據。若是熟悉哈希表的話,能夠很容易看出這種查表方式就是哈希表的直接訪問法。

     如獲取星期名稱,邏輯判斷語句以下:

 1 if(0 == ucDay)
 2 {
 3     pszDayName = "Sunday";
 4 }
 5 else if(1 == ucDay)
 6 {
 7     pszDayName = "Monday";
 8 }
 9 //... ...
10 else if(6 == ucDay)
11 {
12     pszDayName = "Saturday";
13 }
View Code

     而實現一樣的功能,可將這些數據存儲到一個表裏:

1 CHAR *paNumChars[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",  "Saturday"};
2 CHAR *pszDayName = paNumChars[ucDay];
View Code

     相似哈希表特性,表驅動法適用於無需有序遍歷數據,且數據量大小可提早預測的狀況。

     對於過於複雜和龐大的判斷,可將數據存爲文件,須要時加載文件初始化數組,從而在不修改程序的狀況下調整裏面的數值。

     有時,訪問以前須要先進行一次鍵值轉換。如表驅動法表示端口忙閒時,需將槽位端口號映射爲全局編號。所生成的端口數目大小的數組,其下標對應全局端口編號,元素值表示相應端口的忙閒狀態。

1.1.2 索引查找

     有時經過一次鍵值轉換,依然沒法把數據(如英文單詞等)轉爲鍵值。此時可將轉換的對應關係寫到一個索引表裏,即索引訪問。

     如現有100件商品,4位編號,範圍從0000到9999。此時只須要申請一個長度爲100的數組,且對應2位鍵值。但將4位的編號轉換爲2位的鍵值,可能過於複雜或沒有規律,最合適的方法是創建一個保存該轉換關係的索引表。採用索引訪問既節省內存,又方便維護。好比索引A表示經過名稱訪問,索引B表示經過編號訪問。

1.1.3 分段查找

     經過肯定數據所處的範圍肯定分類(下標)。有的數據可分紅若干區間,即具備階梯性,如分數等級。此時可將每一個區間的上限(或下限)存到一個表中,將對應的值存到另外一表中,經過第一個表肯定所處的區段,再由區段下標在第二個表裏讀取相應數值。注意要留意端點,可用二分法查找,另外可考慮經過索引方法來代替。

     如根據分數查績效等級:

 1 #define MAX_GRADE_LEVEL   (INT8U)5
 2 DOUBLE aRangeLimit[MAX_GRADE_LEVEL] = {50.0, 60.0, 70.0, 80.0, 100.0};
 3 CHAR *paGrades[MAX_GRADE_LEVEL] = {"Fail", "Pass", "Credit", "Distinction", "High Distinction"};
 4 
 5 static CHAR* EvaluateGrade(DOUBLE dScore)
 6 {
 7     INT8U ucLevel = 0;
 8     for(; ucLevel < MAX_GRADE_LEVEL; ucLevel++)
 9     {
10         if(dScore < aRangeLimit[ucLevel])
11             return paGrades[ucLevel];
12     }
13     return paGrades[0];
14 }
View Code

     上述兩張表(數組)也可合併爲一張表(結構體數組),以下所示:

 1 typedef struct{
 2     DOUBLE  aRangeLimit;
 3     CHAR    *pszGrade;
 4 }T_GRADE_MAP;
 5 
 6 T_GRADE_MAP gGradeMap[MAX_GRADE_LEVEL] = {
 7     {50.0,              "Fail"},
 8     {60.0,              "Pass"},
 9     {70.0,              "Credit"},
10     {80.0,              "Distinction"},
11     {100.0,             "High Distinction"}
12 };
13 
14 static CHAR* EvaluateGrade(DOUBLE dScore)
15 {
16     INT8U ucLevel = 0;
17     for(; ucLevel < MAX_GRADE_LEVEL; ucLevel++)
18     {
19         if(dScore < gGradeMap[ucLevel].aRangeLimit)
20             return gGradeMap[ucLevel].pszGrade;
21     }
22     return gGradeMap[0].pszGrade;
23 }
View Code

     該表結構已具有的數據庫的雛形,並可擴展支持更爲複雜的數據。其查表方式一般爲索引查找,偶爾也爲分段查找;當索引具備規律性(如連續整數)時,退化爲直接查找。

     使用分段查找法時應注意邊界,將每一分段範圍的上界值都考慮在內。找出全部不在最高一級範圍內的值,而後把剩下的值所有納入最高一級中。有時須要人爲地爲最高一級範圍添加一個上界。

     同時應當心不要錯誤地用「<」來代替「<=」。要保證循環在找出屬於最高一級範圍內的值後恰當地結束,同時也要保證恰當處理範圍邊界。

 

1.2 實戰示例

    本節多數示例取自實際項目。表形式爲一維數組、二維數組和結構體數組;表內容有數據、字符串和函數指針。基於表驅動的思想,表形式和表內容可衍生出豐富的組合。

1.2.1 字符統計

     問題:統計用戶輸入的一串數字中每一個數字出現的次數。

     普通解法主體代碼以下:

 1 INT32U aDigitCharNum[10] = {0}; /* 輸入字符串中各數字字符出現的次數 */
 2 INT32U dwStrLen = strlen(szDigits);
 3 
 4 INT32U dwStrIdx = 0;
 5 for(; dwStrIdx < dwStrLen; dwStrIdx++)
 6 {
 7     switch(szDigits[dwStrIdx])
 8     {
 9         case '1':
10             aDigitCharNum[0]++;
11             break;
12         case '2':
13             aDigitCharNum[1]++;
14             break;
15         //... ...
16         case '9':
17             aDigitCharNum[8]++;
18             break;
19     }
20 }
View Code

     這種解法的缺點顯而易見,既不美觀也不靈活。其問題關鍵在於未將數字字符與數組aDigitCharNum下標直接關聯起來。

     如下示出更簡潔的實現方式: 

1 for(; dwStrIdx < dwStrLen; dwStrIdx++)
2 {
3     aDigitCharNum[szDigits[dwStrIdx] - '0']++;
4 }
View Code

     上述實現考慮到0也爲數字字符。該解法也可擴展至統計全部ASCII可見字符。

1.2.2 月天校驗

     問題:對給定年份和月份的天數進行校驗(需區分平年和閏年)。

     普通解法主體代碼以下:

 1 switch(OnuTime.Month)
 2 {
 3     case 1:
 4     case 3:
 5     case 5:
 6     case 7:
 7     case 8:
 8     case 10:
 9     case 12:
10         if(OnuTime.Day>31 || OnuTime.Day<1)
11         {
12             CtcOamLog(FUNCTION_Pon,"Don't support this Day: %d(1~31)!!!\n", OnuTime.Day);
13             retcode = S_ERROR;
14         }
15         break;
16     case 2:
17         if(((OnuTime.Year%4 == 0) && (OnuTime.Year%100 != 0)) || (OnuTime.Year%400 == 0))
18         {
19             if(OnuTime.Day>29 || OnuTime.Day<1)
20             {
21                 CtcOamLog(FUNCTION_Pon,"Don't support this Day: %d(1~29)!!!\n", OnuTime.Day);
22                 retcode = S_ERROR;
23             }
24         }
25         else
26         {
27             if(OnuTime.Day>28 || OnuTime.Day<1)
28             {
29                 CtcOamLog(FUNCTION_Pon,"Don't support this Day: %d(1~28)!!!\n", OnuTime.Day);
30                 retcode = S_ERROR;
31             }
32         }
33         break;
34     case 4:
35     case 6:
36     case 9:
37     case 11:
38         if(OnuTime.Day>30 || OnuTime.Day<1)
39         {
40             CtcOamLog(FUNCTION_Pon,"Don't support this Day: %d(1~30)!!!\n", OnuTime.Day);
41             retcode = S_ERROR;
42         }
43         break;
44     default:
45         CtcOamLog(FUNCTION_Pon,"Don't support this Month: %d(1~12)!!!\n", OnuTime.Month);
46         retcode = S_ERROR;
47         break;
48 }
View Code

     如下示出更簡潔的實現方式:

 1 #define MONTH_OF_YEAR 12    /* 一年中的月份數 */
 2 
 3 /* 閏年:能被4整除且不能被100整除,或能被400整除 */
 4 #define IS_LEAP_YEAR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0))
 5 
 6 /* 平年中的各月天數,下標對應月份 */
 7 INT8U aDayOfCommonMonth[MONTH_OF_YEAR] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
 8 
 9 INT8U ucMaxDay = 0;
10 if((OnuTime.Month == 2) && (IS_LEAP_YEAR(OnuTime.Year)))
11     ucMaxDay = aDayOfCommonMonth[1] + 1;
12 else
13     ucMaxDay = aDayOfCommonMonth[OnuTime.Month-1];
14 
15 if((OnuTime.Day < 1) || (OnuTime.Day > ucMaxDay)
16 {
17     CtcOamLog(FUNCTION_Pon,"Month %d doesn't have this Day: %d(1~%d)!!!\n",
18               OnuTime.Month, OnuTime.Day, ucMaxDay);
19     retcode = S_ERROR;
20 }
View Code

1.2.3 名稱構造

     問題:根據WAN接口承載的業務類型(Bitmap)構造業務類型名稱字符串。

     普通解法主體代碼以下:

 1 void Sub_SetServerType(INT8U *ServerType, INT16U wan_servertype)
 2 {
 3     if ((wan_servertype & 0x0001) == 0x0001)
 4     {
 5         strcat(ServerType, "_INTERNET");
 6     }
 7     if ((wan_servertype & 0x0002) == 0x0002)
 8     {
 9         strcat(ServerType, "_TR069");
10     }
11     if ((wan_servertype & 0x0004) == 0x0004)
12     {
13         strcat(ServerType, "_VOIP");
14     }
15     if ((wan_servertype & 0x0008) == 0x0008)
16     {
17         strcat(ServerType, "_OTHER");
18     }
19 }
View Code

     如下示出C語言中更簡潔的實現方式:

 1 #define  GET_BIT(var, bit)   (((var) >> (bit)) & 0x1)    /* 獲取var變量第bit位,編號從右至左 */
 2 const CHAR* paSvrNames[] = {"_INTERNET", "_TR069", "_VOIP", "_OTHER"};
 3 const INT8U ucSvrNameNum = sizeof(paSvrNames) / sizeof(paSvrNames[0]);
 4 
 5 VOID SetServerType(CHAR *pszSvrType, INT16U wSvrType)
 6 {
 7     INT8U ucIdx = 0;
 8     for(; ucIdx < ucSvrNameNum; ucIdx++)
 9     {
10         if(1 == GET_BIT(wSvrType, ucIdx))
11             strcat(pszSvrType, paSvrNames[ucIdx]);
12     }
13 }
View Code

     新的實現將數據和邏輯分離,維護起來很是方便。只要邏輯(規則)不變,則惟一可能的改動就是數據(paSvrNames)。

1.2.4 值名解析

     問題:根據枚舉變量取值輸出其對應的字符串,如PORT_FE(1)輸出「Fe」。

 1 //值名映射表結構體定義,用於數值解析器
 2 typedef struct{
 3     INT32U dwElem;    //待解析數值,一般爲枚舉變量
 4     CHAR*  pszName;   //指向數值所對應解析名字符串的指針
 5 }T_NAME_PARSER;
 6 
 7 /******************************************************************************
 8 * 函數名稱:  NameParser
 9 * 功能說明:  數值解析器,將給定數值轉換爲對應的具名字符串
10 * 輸入參數:  VOID *pvMap       :值名映射表數組,含T_NAME_PARSER結構體類型元素
11                                 VOID指針容許用戶在保持成員數目和類型不變的前提下,
12                                 定製更有意義的結構體名和/或成員名。
13              INT32U dwEntryNum :值名映射表數組條目數
14              INT32U dwElem     :待解析數值,一般爲枚舉變量
15              INT8U* pszDefName :缺省具名字符串指針,可爲空
16 * 輸出參數:  NA
17 * 返回值  :  INT8U *: 數值所對應的具名字符串
18              當沒法解析給定數值時,若pszDefName爲空,則返回數值對應的16進制格式
19              字符串;不然返回pszDefName。
20 ******************************************************************************/
21 INT8U *NameParser(VOID *pvMap, INT32U dwEntryNum, INT32U dwElem, INT8U* pszDefName)
22 {
23     CHECK_SINGLE_POINTER(pvMap, "NullPoniter");
24 
25     INT32U dwEntryIdx = 0;
26     for(dwEntryIdx = 0; dwEntryIdx < dwEntryNum; dwEntryIdx++)
27     {
28         T_NAME_PARSER *ptNameParser = (T_NAME_PARSER *)pvMap;
29         if(dwElem == ptNameParser->dwElem)
30         {
31             return ptNameParser->pszName;
32         }
33         //ANSI標準禁止對void指針進行算法操做;GNU標準則指定void*算法操做與char*一致。
34         //若考慮移植性,可將pvMap類型改成INT8U*,或定義INT8U*局部變量指向pvMap。
35         pvMap += sizeof(T_NAME_PARSER);
36     }
37 
38     if(NULL != pszDefName)
39     {
40         return pszDefName;
41     }
42     else
43     {
44         static INT8U szName[12] = {0}; //Max:"0xFFFFFFFF"
45         sprintf(szName, "0x%X", dwElem);
46         return szName;
47     }
48 }
View Code

     如下給出NameParser的簡單應用示例:

 1 //UNI端口類型值名映射表結構體定義
 2 typedef struct{
 3     INT32U dwPortType;
 4     INT8U* pszPortName;
 5 }T_PORT_NAME;
 6 //UNI端口類型解析器
 7 T_PORT_NAME gUniNameMap[] = {
 8     {1,      "Fe"},
 9     {3,      "Pots"},
10     {99,     "Vuni"}
11 };
12 const INT32U UNI_NAM_MAP_NUM = (INT32U)(sizeof(gUniNameMap)/sizeof(T_PORT_NAME));
13 VOID NameParserTest(VOID)
14 {
15     INT8U ucTestIndex = 1;
16 
17     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
18            strcmp("Unknown", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 0, "Unknown")) ? "ERROR" : "OK");
19     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
20            strcmp("DefName", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 0, "DefName")) ? "ERROR" : "OK");
21     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
22            strcmp("Fe", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 1, "Unknown")) ? "ERROR" : "OK");
23     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
24            strcmp("Pots", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 3, "Unknown")) ? "ERROR" : "OK");
25     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
26            strcmp("Vuni", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 99, NULL)) ? "ERROR" : "OK");
27     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
28            strcmp("Unknown", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 255, "Unknown")) ? "ERROR" : "OK");
29     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
30            strcmp("0xABCD", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 0xABCD, NULL)) ? "ERROR" : "OK");
31     printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
32            strcmp("NullPoniter", NameParser(NULL, UNI_NAM_MAP_NUM, 0xABCD, NULL)) ? "ERROR" : "OK");
33 }
View Code

     gUniNameMap在實際項目中有十餘個條目,若採用邏輯鏈實現將很是冗長。

1.2.5 取值映射

     問題:不一樣模塊間同一參數枚舉值取值可能有所差別,須要適配。

     此處再也不給出普通的switch…case或if…else if…else結構,而直接示出如下表驅動實現:

 1 typedef struct{
 2     PORTSTATE loopMEState;
 3     PORTSTATE loopMIBState;
 4 }LOOPMAPSTRUCT;
 5 
 6 static LOOPMAPSTRUCT s_CesLoop[] = {
 7     {NO_LOOP,                  e_ds1_looptype_noloop},
 8     {PAYLOAD_LOOP,             e_ds1_looptype_PayloadLoop},
 9     {LINE_LOOP,                e_ds1_looptype_LineLoop},
10     {PON_LOOP,                 e_ds1_looptype_OtherLoop},
11     {CES_LOOP,                 e_ds1_looptype_InwardLoop}};
12 
13 PORTSTATE ConvertLoopMEStateToMIBState(PORTSTATE vPortState)
14 {
15     INT32U num = 0, ii;
16     
17     num = ARRAY_NUM(s_CesLoop);
18     for(ii = 0; ii < num; ii++)
19     {
20         if(vPortState == s_CesLoop[ii].loopMEState)
21             return s_CesLoop[ii].loopMIBState;
22     }
23     return e_ds1_looptype_noloop;
24 }
View Code

     相應地,從loopMIBState映射到loopMEState須要定義一個ConvertLoopMIBStateToMEState函數。更進一步,全部相似的一對一映射關係都必須如上的映射(轉換)函數,至關繁瑣。

     事實上,從抽象層面看,該映射關係很是簡單。提取共性後定義帶參數宏,以下所示:

 1 /**********************************************************
 2 * 功能描述:進行二維數組映射表的一對一映射,用於參數適配
 3 * 參數說明:map        -- 二維數組映射表
 4             elemSrc    -- 映射源,即待映射的元素值
 5             elemDest   -- 映射源對應的映射結果
 6             direction  -- 映射方向字節,表示從數組哪列映射至哪列。
 7                           高4位對應映射源列,低4位對應映射結果列。
 8             defaultVal -- 映射失敗時置映射結果爲缺省值
 9 * 示例:    ARRAY_MAPPER(gCesLoopMap, 3, ucLoop, 0x10, NO_LOOP);
10             則ucLoop = 2(LINE_LOOP)
11 **********************************************************/
12 #define ARRAY_MAPPER(map, elemSrc, elemDest, direction, defaultVal) do{\
13     INT8U ucMapIdx = 0, ucMapNum = 0; \
14     ucMapNum = sizeof(map)/sizeof(map[0]); \
15     for(ucMapIdx = 0; ucMapIdx < ucMapNum; ucMapIdx++) \
16     { \
17         if((elemSrc) == map[ucMapIdx][((direction)&0xF0)>>4]) \
18         { \
19             elemDest = map[ucMapIdx][(direction)&0x0F]; \
20             break; \
21         } \
22     } \
23     if(ucMapIdx == ucMapNum) \
24     { \
25         elemDest = (defaultVal); \
26     } \
27 }while(0)
View Code

    參數取值轉換時直接調用統一的映射器宏,以下:

1 static INT8U gCesLoopMap[][2] = {
2     {NO_LOOP,                  e_ds1_looptype_noloop},
3     {PAYLOAD_LOOP,             e_ds1_looptype_PayloadLoop},
4     {LINE_LOOP,                e_ds1_looptype_LineLoop},
5     {PON_LOOP,                 e_ds1_looptype_OtherLoop},
6     {CES_LOOP,                 e_ds1_looptype_InwardLoop}};
7 
8 ARRAY_MAPPER(gCesLoopMap, tPara.dwParaVal[0], dwLoopConf, 0x01, e_ds1_looptype_noloop);
View Code

     另舉一例:

 1 #define  CES_DEFAULT_JITTERBUF        (INT32U)2000   /* 默認jitterbuf爲2000us,而1幀=125us */
 2 #define  CES_JITTERBUF_STEP           (INT32U)125    /* jitterbuf步長爲125us,即1幀 */
 3 #define  CES_DEFAULT_QUEUESIZE        (INT32U)5      
 4 #define  CES_DEFAULT_MAX_QUEUESIZE    (INT32U)7
 5 
 6 #define  ARRAY_NUM(array)             (sizeof(array) / sizeof((array)[0]))  /* 數組元素個數 */
 7 typedef struct{
 8     INT32U  dwJitterBuffer;
 9     INT32U  dwFramePerPkt;
10     INT32U  dwQueueSize;
11 }QUEUE_SIZE_MAP;
12 /* gCesQueueSizeMap也能夠(JitterBuffer / FramePerPkt)值爲索引,更加緊湊 */
13 static QUEUE_SIZE_MAP gCesQueueSizeMap[]= {
14        {1,1,1},  {1,2,1},  {2,1,2},  {2,2,1},
15        {3,1,3},  {3,2,1},  {4,1,3},  {4,2,1},
16        {5,1,4},  {5,2,3},  {6,1,4},  {6,2,3},
17        {7,1,4},  {7,2,3},  {8,1,4},  {8,2,3},
18        {9,1,5},  {9,2,4},  {10,1,5}, {10,2,4},
19        {11,1,5}, {11,2,4}, {12,1,5}, {12,2,4},
20        {13,1,5}, {13,2,4}, {14,1,5}, {14,2,4},
21        {15,1,5}, {15,2,4}, {16,1,5}, {16,2,4},
22        {17,1,6}, {17,2,5}, {18,1,6}, {18,2,5},
23        {19,1,6}, {19,2,5}, {20,1,6}, {20,2,5},
24        {21,1,6}, {21,2,5}, {22,1,6}, {22,2,5},
25        {23,1,6}, {23,2,5}, {24,1,6}, {24,2,5},
26        {25,1,6}, {25,2,5}, {26,1,6}, {26,2,5},
27        {27,1,6}, {27,2,5}, {28,1,6}, {28,2,5},
28        {29,1,6}, {29,2,5}, {30,1,6}, {30,2,5},
29        {31,1,6}, {31,2,5}, {32,1,6}, {32,2,5}};
30 /**********************************************************
31 * 函數名稱: CalcQueueSize
32 * 功能描述: 根據JitterBuffer和FramePerPkt計算QueueSize
33 * 注意事項: 配置的最大緩存深度 
34 *            = 2 * JitterBuffer / FramePerPkt
35 *            = 2 * N Packet = 2 ^ QueueSize
36 *            JitterBuffer爲125us幀速率的倍數,
37 *            FramePerPkt爲每一個分組的幀數,
38 *            QueueSize向上取整,最大爲7。
39 **********************************************************/
40 INT32U CalcQueueSize(INT32U dwJitterBuffer, INT32U dwFramePerPkt)
41 {
42     INT8U ucIdx = 0, ucNum = 0;
43 
44     //本函數暫時僅考慮E1
45     ucNum = ARRAY_NUM(gCesQueueSizeMap);
46     for(ucIdx = 0; ucIdx < ucNum; ucIdx++)
47     {
48        if((dwJitterBuffer == gCesQueueSizeMap[ucIdx].dwJitterBuffer) &&
49           (dwFramePerPkt == gCesQueueSizeMap[ucIdx].dwFramePerPkt))
50        {
51             return gCesQueueSizeMap[ucIdx].dwQueueSize;
52        }
53     }
54     
55     return CES_DEFAULT_MAX_QUEUESIZE;
56 }
View Code

1.2.6 版本控制

     問題:控制OLT與ONU之間的版本協商。ONU本地設置三比特控制字,其中bit2(MSB)~bit0(LSB)分別對應0x2一、0x30和0xAA版本號;且bitX爲0表示上報對應版本號,bitX爲1表示不上報對應版本號。其餘版本號如0x20、0x13和0x1必須上報,即不受控制。

     最初的實現採用if…else if…else結構,代碼很是冗長,以下:

 1 pstSendTlv->ucLength = 0x1f;
 2 if (gOamCtrlCode == 0)
 3 {
 4     vosMemCpy(pstSendTlv->aucVersionList, ctc_oui, 3);
 5     pstSendTlv->aucVersionList[3] = 0x30;
 6     vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);    
 7     pstSendTlv->aucVersionList[7] = 0x21;
 8     vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);
 9     pstSendTlv->aucVersionList[11] = 0x20;
10     vosMemCpy(&(pstSendTlv->aucVersionList[12]), ctc_oui, 3);
11     pstSendTlv->aucVersionList[15] = 0x13;
12     vosMemCpy(&(pstSendTlv->aucVersionList[16]), ctc_oui, 3);
13     pstSendTlv->aucVersionList[19] = 0x01;
14     vosMemCpy(&(pstSendTlv->aucVersionList[20]), ctc_oui, 3);
15     pstSendTlv->aucVersionList[23] = 0xaa;
16 }
17 else if (gOamCtrlCode == 1)
18 {
19     vosMemCpy(pstSendTlv->aucVersionList, ctc_oui, 3);
20     pstSendTlv->aucVersionList[3] = 0x30;
21     vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);    
22     pstSendTlv->aucVersionList[7] = 0x21;
23     vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);
24     pstSendTlv->aucVersionList[11] = 0x20;
25     vosMemCpy(&(pstSendTlv->aucVersionList[12]), ctc_oui, 3);
26     pstSendTlv->aucVersionList[15] = 0x13;
27     vosMemCpy(&(pstSendTlv->aucVersionList[16]), ctc_oui, 3);
28     pstSendTlv->aucVersionList[19] = 0x01;
29 }
30 //此處省略gOamCtrlCode == 2~6的處理代碼
31 else if (gOamCtrlCode == 7)
32 {
33     vosMemCpy(&(pstSendTlv->aucVersionList), ctc_oui, 3);    
34     pstSendTlv->aucVersionList[3] = 0x20;
35     vosMemCpy(&(pstSendTlv->aucVersionList[4]), ctc_oui, 3);
36     pstSendTlv->aucVersionList[7] = 0x13;
37     vosMemCpy(&(pstSendTlv->aucVersionList[8]), ctc_oui, 3);
38     pstSendTlv->aucVersionList[11] = 0x01;
39 }
View Code

    如下示出C語言中更簡潔的實現方式(基於二維數組):

 1 /**********************************************************************
 2 * 版本控制字數組定義
 3 * gOamCtrlCode:   Bitmap控制字。Bit-X爲0時上報對應版本,Bit-X爲1時屏蔽對應版本。
 4 * CTRL_VERS_NUM:  可控版本個數。
 5 * CTRL_CODE_NUM:  控制字個數。與CTRL_VERS_NUM有關。
 6 * gOamVerCtrlMap: 版本控制字數組。行對應控制字,列對應可控版本。
 7                   元素值爲0時不上報對應版本,元素值非0時上報該元素值。
 8 * Note: 該數組旨在實現「數據與控制隔離」。後續若要新增可控版本,只需修改
 9                   -- CTRL_VERS_NUM
10                   -- gOamVerCtrlMap新增行(控制字)
11                   -- gOamVerCtrlMap新增列(可控版本)
12 **********************************************************************/
13 #define CTRL_VERS_NUM    3
14 #define CTRL_CODE_NUM    (1<<CTRL_VERS_NUM)
15 u8_t gOamVerCtrlMap[CTRL_CODE_NUM][CTRL_VERS_NUM] = {
16  /* Ver21         Ver30        VerAA */
17     {0x21,         0x30,        0xaa},    /*gOamCtrlCode = 0*/
18     {0x21,         0x30,          0 },    /*gOamCtrlCode = 1*/
19     {0x21,           0,         0xaa},    /*gOamCtrlCode = 2*/
20     {0x21,           0,           0 },    /*gOamCtrlCode = 3*/
21     {  0,          0x30,        0xaa},    /*gOamCtrlCode = 4*/
22     {  0,          0x30,          0 },    /*gOamCtrlCode = 5*/
23     {  0,            0,         0xaa},    /*gOamCtrlCode = 6*/
24     {  0,            0,           0 }     /*gOamCtrlCode = 7*/
25 };
26 #define INFO_TYPE_VERS_LEN    7  /* InfoType + Length + OUI + ExtSupport + Version */
27 
28 u8_t verIdx = 0;
29 u8_t index = 0;
30 for(verIdx = 0; verIdx < CTRL_VERS_NUM; verIdx++)
31 {
32     if(gOamVerCtrlMap[gOamCtrlCode][verIdx] != 0)
33     {
34         vosMemCpy(&pstSendTlv->aucVersionList[index], ctc_oui, 3);
35         index += 3;
36         pstSendTlv->aucVersionList[index++] = gOamVerCtrlMap[gOamCtrlCode][verIdx];
37     }
38 }
39 vosMemCpy(&pstSendTlv->aucVersionList[index], ctc_oui, 3);
40 index += 3;
41 pstSendTlv->aucVersionList[index++] = 0x20;
42 vosMemCpy(&pstSendTlv->aucVersionList[index], ctc_oui, 3);
43 index += 3;
44 pstSendTlv->aucVersionList[index++] = 0x13;
45 vosMemCpy(&pstSendTlv->aucVersionList[index], ctc_oui, 3);
46 index += 3;
47 pstSendTlv->aucVersionList[index++] = 0x01;
48 
49 pstSendTlv->ucLength = INFO_TYPE_VERS_LEN + index;
View Code

1.2.7 消息處理

     問題:終端輸入不一樣的打印命令,調用相應的打印函數,以控制不一樣級別的打印。

     這是一段消息(事件)驅動程序。本模塊接收其餘模塊(如串口驅動)發送的消息,根據消息中的打印級別字符串和開關模式,調用不一樣函數進行處理。常見的實現方法以下:

 1 void logall(void)
 2 {
 3     g_log_control[0] = 0xFFFFFFFF;
 4 }
 5 
 6 void noanylog(void)
 7 {
 8     g_log_control[0] = 0;
 9 }
10 
11 void logOam(void)
12 {
13     g_log_control[0] |= (0x01 << FUNCTION_Oam);
14 }
15 void nologOam(void)
16 {
17     g_log_control[0] &= ~(0x01 << FUNCTION_Oam);
18 }
19 //... ...
20 void logExec(char *name, INT8U enable)
21 {
22     CtcOamLog(FUNCTION_Oam,"log %s %d\n",name,enable);
23     if (enable == 1) /*log*/
24     {
25         if (strcasecmp(name,"all") == 0) { /*字符串比較,不區分大小寫*/
26             logall();
27         } else if (strcasecmp(name,"oam") == 0) {
28             logOam();
29         } else if (strcasecmp(name,"pon") == 0) {
30             logPon();
31         //... ...
32         } else if (strcasecmp(name,"version") == 0) {
33             logVersion();
34     }
35     else if (enable == 0) /*nolog*/
36     {
37         if (strcasecmp(name,"all") == 0) {
38             noanylog();
39         } else if (strcasecmp(name,"oam") == 0) {
40             nologOam();
41         } else if (strcasecmp(name,"pon") == 0) {
42             nologPon();
43         //... ...
44         } else if (strcasecmp(name,"version") == 0) {
45             nologVersion();
46     }
47     else
48     {
49         printf("bad log para\n");
50     }
51 }
View Code

     如下示出C語言中更簡潔的實現方式:

 1 typedef struct{
 2     OAM_LOG_OFF = (INT8U)0,
 3     OAM_LOG_ON  = (INT8U)1
 4 }E_OAM_LOG_MODE;
 5 typedef FUNC_STATUS (*OamLogHandler)(VOID);
 6 typedef struct{
 7     CHAR           *pszLogCls;    /* 打印級別 */
 8     E_OAM_LOG_MODE eLogMode;      /* 打印模式 */
 9     OamLogHandler  fnLogHandler;  /* 打印函數 */
10 }T_OAM_LOG_MAP;
11 
12 T_OAM_LOG_MAP gOamLogMap[] = {
13     {"all",         OAM_LOG_OFF,       noanylog},
14     {"oam",         OAM_LOG_OFF,       nologOam},
15     //... ...
16     {"version",     OAM_LOG_OFF,       nologVersion},
17     
18     {"all",         OAM_LOG_ON,        logall},
19     {"oam",         OAM_LOG_ON,        logOam},
20     //... ...
21     {"version",     OAM_LOG_ON,        logVersion}
22 };
23 INT32U gOamLogMapNum = sizeof(gOamLogMap) / sizeof(T_OAM_LOG_MAP);
24 
25 VOID logExec(CHAR *pszName, INT8U ucSwitch)
26 {
27     INT8U ucIdx = 0;
28     for(; ucIdx < gOamLogMapNum; ucIdx++)
29     {
30         if((ucSwitch == gOamLogMap[ucIdx].eLogMode) &&
31            (!strcasecmp(pszName, gOamLogMap[ucIdx].pszLogCls));
32         {
33             gOamLogMap[ucIdx].fnLogHandler();
34             return;
35         }
36     }
37     if(ucIdx == gOamLogMapNum)
38     {
39         printf("Unknown LogClass(%s) or LogMode(%d)!\n", pszName, ucSwitch);
40         return;
41     }
42 }
View Code

     這種表驅動消息處理實現的優勢以下:

  • 加強可讀性,消息如何處理從表中一目瞭然。
  • 加強可擴展性。更容易修改,要增長新的消息,只要修改數據便可,不須要修改流程。
  • 下降複雜度。經過把程序邏輯的複雜度轉移到人類更容易處理的數據中來,從而達到控制複雜度的目標。
  • 主幹清晰,代碼重用。

     若各索引爲順序枚舉值,則創建多維數組(每維對應一個索引),根據下標直接定位處處理函數,效率會更高。

     注意,考慮到本節實例中logOam/logPon或nologOam/nologPon等函數本質上是基於打印級別的比特操做,所以可進一步簡化。如下例舉其類似實現:

 1 /* 日誌控制類型定義 */
 2 typedef enum
 3 {
 4     LOG_NORM = 0,        /* 未分類日誌,可用於通用日誌 */
 5     LOG_FRM,             /* Frame,OMCI幀日誌 */
 6     LOG_PON,             /* Pon,光鏈路相關日誌 */
 7     LOG_ETH,             /* Ethernet,Layer2以太網日誌 */
 8     LOG_NET,             /* Internet,Layer3網絡日誌 */
 9     LOG_MULT,            /* Multicast,組播日誌 */
10     LOG_QOS,             /* QOS,流量日誌 */
11     LOG_CES,             /* Ces,TDM電路仿真日誌 */
12     LOG_VOIP,            /* Voip,語音日誌 */
13     LOG_ALM,             /* Alarm,告警日誌 */
14     LOG_PERF,            /* Performance,性能統計日誌 */
15     LOG_VER,             /* Version,軟件升級日誌 */
16     LOG_XDSL,            /* xDsl日誌 */
17     LOG_DB,              /* 數據庫操做日誌 */
18     //新日誌類型在此處擴展,共支持32種日誌類型
19     LOG_ALL = UINT_MAX   /* 全部日誌類型 */
20 }E_LOG_TYPE;
21 
22 /*****************************************************************************
23  * 變量名稱:gOmciLogCtrl
24  * 做用描述:OMCI日誌控制字,BitMap格式(比特編號從LSB至MSB依次爲Bit0->BitN)。
25  *           Bit0~N分別對應E_LOG_TYPE各枚舉值(除LOG_ALL外)。
26  *           BitX爲0時關閉日誌類型對應的日誌功能,BitX爲1時則予以打開。
27  * 變量範圍:該變量爲四字節整型靜態全局變量,即支持32種日誌類型。
28  * 訪問說明:經過GetOmciLogCtrl/SetOmciLogCtrl/OmciLogCtrl函數訪問/設置控制字。
29  *****************************************************************************/
30 static INT32U gOmciLogCtrl = 0;
31 
32 //日誌類型字符串數組,下標爲各字符串所對應的日誌類型枚舉值。
33 static const INT8U* paLogTypeName[] = {
34     "Norm",        "Frame",   "Pon",  "Ethernet",  "Internet",
35     "Multicast",   "Qos",     "Ces",  "Voip",      "Alarm",
36     "Performance", "Version", "Xdsl",  "Db"
37 };
38 static const INT8U  ucLogTypeNameNum = sizeof(paLogTypeName) / sizeof(paLogTypeName[0]);
39 
40 static VOID SetGlobalLogCtrl(E_LOG_TYPE eLogType, INT8U ucLogSwitch)
41 {
42     if(LOG_ON == ucLogSwitch)
43         gOmciLogCtrl = LOG_ALL;
44     else
45         gOmciLogCtrl = 0;
46 }
47 static VOID SetSpecificLogCtrl(E_LOG_TYPE eLogType, INT8U ucLogSwitch)
48 {
49     if(LOG_ON == ucLogSwitch)
50         SET_BIT(gOmciLogCtrl, eLogType);
51     else
52         CLR_BIT(gOmciLogCtrl, eLogType);
53 }
54 
55 VOID OmciLogCtrl(CHAR *pszLogType, INT8U ucLogSwitch)
56 {
57     if(0 == strncasecmp(pszLogType, "All", LOG_TYPE_CMP_LEN))
58     {
59         SetGlobalLogCtrl(LOG_ALL, ucLogSwitch);
60         return;
61     }
62 
63     INT8U ucIdx = 0;
64     for(ucIdx = 0; ucIdx < ucLogTypeNameNum; ucIdx++)
65     {
66         if(0 == strncasecmp(pszLogType, paLogTypeName[ucIdx], LOG_TYPE_CMP_LEN))
67         {
68             SetSpecificLogCtrl(ucIdx, ucLogSwitch);
69             printf("LogType: %s, LogSwitch: %s\n", paLogTypeName[ucIdx],
70                    (1==ucLogSwitch)?"On":"Off");
71             return;
72         }
73     }
74 
75     OmciLogHelp();
76 }
View Code

1.2.8 掩碼錶

參見《採用掩碼方式簡化產品國家地區支持能力的表示》一文。

該例實現中用到消息、掩碼、函數指針等概念。

 

2 編程思想

     表驅動法屬於數據驅動編程的一種,其核心思想在《Unix編程藝術》和《代碼大全2》中均有闡述。二者均認爲人類閱讀複雜數據結構遠比複雜的控制流程容易,即相對於程序邏輯,人類更擅長於處理數據。

     本節將由Unix設計原則中的「分離原則」和「表示原則」展開。

 

分離原則:策略同機制分離,接口同引擎分離

     機制即提供的功能;策略即如何使用功能。

     策略的變化要遠遠快於機制的變化。將二者分離,可使機制相對保持穩定,而同時支持策略的變化。

     代碼大全中提到「隔離變化」的概念,以及設計模式中提到的將易變化的部分和不易變化的部分分離也是這個思路。

表示原則:把知識疊入數據以求邏輯質樸而健壯

     即便最簡單的程序邏輯讓人類來驗證也很困難,但就算是很複雜的數據,對人類來講,仍是相對容易推導和建模的。數據比編程邏輯更容易駕馭。在複雜數據和複雜代碼中選擇,寧肯選擇前者。更進一步,在設計中,應該主動將代碼的複雜度轉移到數據中去(參考「版本控制」)。

     在「消息處理」示例中,每一個消息處理的邏輯不變,但消息多是變化的。將容易變化的消息和不容易變化的查找邏輯分離,即「隔離變化」。此外,該例也體現消息內部的處理邏輯(機制)和不一樣的消息處理(策略)分離。

 

     數據驅動編程能夠應用於:

  • 函數級設計,如本文示例。
  • 程序級設計,如用表驅動法實現狀態機。
  • 系統級設計,如DSL。

     注意,數據驅動編程不是全新的編程模型,只是一種設計思路,在Unix/Linux開源社區應用不少。數據驅動編程中,數據不但表示某個對象的狀態,實際上還定義程序的流程,這點不一樣於面向對象設計中的數據「封裝」。

 

3 附錄

3.1 網友觀點

     (如下觀點摘自博客園網友「七心葵」的回帖,很是具備啓發性。)

     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——向上拋出之道——將複雜的易變的邏輯做爲數據或更高層代碼拋給上層!

 

3.2 函數指針

     「消息處理」一節示例中的函數指針有點插件結構的味道。可對這些插件進行方便替換,新增,刪除,從而改變程序的行爲。而這種改變,對事件處理函數的查找又是隔離的(隔離變化)。

     函數指針很是有用,但使用時需注意其缺陷:沒法檢查參數(parameter)和返回值(return value)的類型。由於函數已經退化成指針,而指針不攜帶這些類型信息。缺乏類型檢查,當參數或返回值不一致時,可能會形成嚴重的錯誤。

     例如,定義三個函數,分別具備兩個參數:

     int max(int x, int y)  {  return x>y?x:y;  }

     int min(int x, int y)  {  return x<y?x:y;  }

     int add(int x, int y)  {  return x+y;  }

     而處理函數卻定義爲:

     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語言中使用函數指針時,必定要當心"類型陷阱"。

相關文章
相關標籤/搜索