1. 程式人生 > 其它 >【轉】C語言表驅動法程式設計實踐

【轉】C語言表驅動法程式設計實踐

來源:C語言表驅動法程式設計實踐(精華帖,建議收藏並實踐) (qq.com)

資料壓倒一切。如果選擇了正確的資料結構並把一切組織的井井有條,正確的演算法就不言自明。程式設計的核心是資料結構,而不是演算法。

——Rob Pike

說明

本文基於這樣的認識:資料是易變的,邏輯是穩定的。

本文例舉的程式設計實現多為程式碼片段,但不影響描述的完整性。

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

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

1 概念提出

所謂表驅動法(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節“實戰示例”內容。

1.1 查表方式

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

1.1.1 直接查詢

即直接通過陣列下標獲取到資料。如果熟悉雜湊表的話,可以很容易看出這種查表方式就是雜湊表的直接訪問法。

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

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];

類似雜湊表特性,表驅動法適用於無需有序遍歷資料,且資料量大小可提前預測的情況。

對於過於複雜和龐大的判斷,可將資料存為檔案,需要時載入檔案初始化陣列,從而在不修改程式的情況下調整裡面的數值。

有時,訪問之前需要先進行一次鍵值轉換。如表驅動法表示埠忙閒時,需將槽位埠號對映為全域性編號。所生成的埠數目大小的陣列,其下標對應全域性埠編號,元素值表示相應埠的忙閒狀態。

1.1.2 索引查詢

有時通過一次鍵值轉換,依然無法把資料(如英文單詞等)轉為鍵值。此時可將轉換的對應關係寫到一個索引表裡,即索引訪問。

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

1.1.3 分段查詢

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

如根據分數查績效等級:

#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 < MAX_GRADE_LEVEL; ucLevel++)
    {
        if(dScore < aRangeLimit[ucLevel])
            return paGrades[ucLevel];
    }
    return paGrades[0];
}

上述兩張表(陣列)也可合併為一張表(結構體陣列),如下所示:

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 < MAX_GRADE_LEVEL; ucLevel++)
    {
        if(dScore < gGradeMap[ucLevel].aRangeLimit)
            return gGradeMap[ucLevel].pszGrade;
    }
    return gGradeMap[0].pszGrade;
}

該表結構已具備的資料庫的雛形,並可擴充套件支援更為複雜的資料。其查表方式通常為索引查詢,偶爾也為分段查詢;當索引具有規律性(如連續整數)時,退化為直接查詢。

使用分段查詢法時應注意邊界,將每一分段範圍的上界值都考慮在內。找出所有不在最高一級範圍內的值,然後把剩下的值全部歸入最高一級中。有時需要人為地為最高一級範圍新增一個上界。

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

1.2 實戰示例

本節多數示例取自實際專案。表形式為一維陣列、二維陣列和結構體陣列;表內容有資料、字串和函式指標。基於表驅動的思想,表形式和表內容可衍生出豐富的組合。

1.2.1 字元統計

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

普通解法主體程式碼如下:

INT32U aDigitCharNum[10] = {0}; /* 輸入字串中各數字字元出現的次數 */
INT32U dwStrLen = strlen(szDigits);

INT32U dwStrIdx = 0;
for(; dwStrIdx < dwStrLen; dwStrIdx++)
{
    switch(szDigits[dwStrIdx])
    {
        case '1':
            aDigitCharNum[0]++;
            break;
        case '2':
            aDigitCharNum[1]++;
            break;
        //... ...
        case '9':
            aDigitCharNum[8]++;
            break;
    }
}

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

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

for(; dwStrIdx < dwStrLen; dwStrIdx++)
{
    aDigitCharNum[szDigits[dwStrIdx] - '0']++;
}

上述實現考慮到0也為數字字元。該解法也可擴充套件至統計所有ASCII可見字元。

1.2.2 月天校驗

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

普通解法主體程式碼如下:

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)!!!\n", 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)!!!\n", OnuTime.Day);
                retcode = S_ERROR;
            }
        }
        else
        {
            if(OnuTime.Day>28 || OnuTime.Day<1)
            {
                CtcOamLog(FUNCTION_Pon,"Don't support this Day: %d(1~28)!!!\n", 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)!!!\n", OnuTime.Day);
            retcode = S_ERROR;
        }
        break;
    default:
        CtcOamLog(FUNCTION_Pon,"Don't support this Month: %d(1~12)!!!\n", 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)!!!\n",
              OnuTime.Month, OnuTime.Day, ucMaxDay);
    retcode = S_ERROR;
}

1.2.3 名稱構造

問題:根據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 < ucSvrNameNum; ucIdx++)
    {
        if(1 == GET_BIT(wSvrType, ucIdx))
            strcat(pszSvrType, paSvrNames[ucIdx]);
    }
}

新的實現將資料和邏輯分離,維護起來非常方便。只要邏輯(規則)不變,則唯一可能的改動就是資料(paSvrNames)。

1.2.4 值名解析

問題:根據列舉變數取值輸出其對應的字串,如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 < dwEntryNum; dwEntryIdx++)
    {
        T_NAME_PARSER *ptNameParser = (T_NAME_PARSER *)pvMap;
        if(dwElem == ptNameParser->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]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("Unknown", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 0, "Unknown")) ? "ERROR" : "OK");
    printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("DefName", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 0, "DefName")) ? "ERROR" : "OK");
    printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("Fe", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 1, "Unknown")) ? "ERROR" : "OK");
    printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("Pots", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 3, "Unknown")) ? "ERROR" : "OK");
    printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("Vuni", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 99, NULL)) ? "ERROR" : "OK");
    printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("Unknown", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 255, "Unknown")) ? "ERROR" : "OK");
    printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("0xABCD", NameParser(gUniNameMap, UNI_NAM_MAP_NUM, 0xABCD, NULL)) ? "ERROR" : "OK");
    printf("[%s]<Test Case %u> Result: %s!\n", __FUNCTION__, ucTestIndex++,
           strcmp("NullPoniter", NameParser(NULL, UNI_NAM_MAP_NUM, 0xABCD, NULL)) ? "ERROR" : "OK");
}

gUniNameMap在實際專案中有十餘個條目,若採用邏輯鏈實現將非常冗長。

1.2.5 取值對映

問題:不同模組間同一引數列舉值取值可能有所差異,需要適配。

此處不再給出普通的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 < num; ii++)
    {
        if(vPortState == s_CesLoop[ii].loopMEState)
            return s_CesLoop[ii].loopMIBState;
    }
    return e_ds1_looptype_noloop;
}

相應地,從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 < ucMapNum; ucMapIdx++) \
    { \
        if((elemSrc) == map[ucMapIdx][((direction)&0xF0)>>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 < ucNum; ucIdx++)
    {
       if((dwJitterBuffer == gCesQueueSizeMap[ucIdx].dwJitterBuffer) &&
          (dwFramePerPkt == gCesQueueSizeMap[ucIdx].dwFramePerPkt))
       {
            return gCesQueueSizeMap[ucIdx].dwQueueSize;
       }
    }
    
    return CES_DEFAULT_MAX_QUEUESIZE;
}

1.2.6 版本控制

問題:控制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<<CTRL_VERS_NUM)
u8_t gOamVerCtrlMap[CTRL_CODE_NUM][CTRL_VERS_NUM] = {
 /* Ver21         Ver30        VerAA */
    {0x21,         0x30,        0xaa},    /*gOamCtrlCode = 0*/
    {0x21,         0x30,          0 },    /*gOamCtrlCode = 1*/
    {0x21,           0,         0xaa},    /*gOamCtrlCode = 2*/
    {0x21,           0,           0 },    /*gOamCtrlCode = 3*/
    {  0,          0x30,        0xaa},    /*gOamCtrlCode = 4*/
    {  0,          0x30,          0 },    /*gOamCtrlCode = 5*/
    {  0,            0,         0xaa},    /*gOamCtrlCode = 6*/
    {  0,            0,           0 }     /*gOamCtrlCode = 7*/
};
#define INFO_TYPE_VERS_LEN    7  /* InfoType + Length + OUI + ExtSupport + Version */

u8_t verIdx = 0;
u8_t index = 0;
for(verIdx = 0; verIdx < CTRL_VERS_NUM; verIdx++)
{
    if(gOamVerCtrlMap[gOamCtrlCode][verIdx] != 0)
    {
        vosMemCpy(&pstSendTlv->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;

1.2.7 訊息處理

問題:終端輸入不同的列印命令,呼叫相應的列印函式,以控制不同級別的列印。

這是一段訊息(事件)驅動程式。本模組接收其他模組(如串列埠驅動)傳送的訊息,根據訊息中的列印級別字串和開關模式,呼叫不同函式進行處理。常見的實現方法如下:

void logall(void)
{
    g_log_control[0] = 0xFFFFFFFF;
}

void noanylog(void)
{
    g_log_control[0] = 0;
}

void logOam(void)
{
    g_log_control[0] |= (0x01 << FUNCTION_Oam);
}
void nologOam(void)
{
    g_log_control[0] &= ~(0x01 << FUNCTION_Oam);
}
//... ...
void logExec(char *name, INT8U enable)
{
    CtcOamLog(FUNCTION_Oam,"log %s %d\n",name,enable);
    if (enable == 1) /*log*/
    {
        if (strcasecmp(name,"all") == 0) { /*字串比較,不區分大小寫*/
            logall();
        } else if (strcasecmp(name,"oam") == 0) {
            logOam();
        } else if (strcasecmp(name,"pon") == 0) {
            logPon();
        //... ...
        } else if (strcasecmp(name,"version") == 0) {
            logVersion();
    }
    else if (enable == 0) /*nolog*/
    {
        if (strcasecmp(name,"all") == 0) {
            noanylog();
        } else if (strcasecmp(name,"oam") == 0) {
            nologOam();
        } else if (strcasecmp(name,"pon") == 0) {
            nologPon();
        //... ...
        } else if (strcasecmp(name,"version") == 0) {
            nologVersion();
    }
    else
    {
        printf("bad log para\n");
    }
}

以下示出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 < gOamLogMapNum; ucIdx++)
    {
        if((ucSwitch == gOamLogMap[ucIdx].eLogMode) &&
           (!strcasecmp(pszName, gOamLogMap[ucIdx].pszLogCls));
        {
            gOamLogMap[ucIdx].fnLogHandler();
            return;
        }
    }
    if(ucIdx == gOamLogMapNum)
    {
        printf("Unknown LogClass(%s) or LogMode(%d)!\n", pszName, ucSwitch);
        return;
    }
}

這種表驅動訊息處理實現的優點如下:

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 < ucLogTypeNameNum; ucIdx++)
    {
        if(0 == strncasecmp(pszLogType, paLogTypeName[ucIdx], LOG_TYPE_CMP_LEN))
        {
            SetSpecificLogCtrl(ucIdx, ucLogSwitch);
            printf("LogType: %s, LogSwitch: %s\n", paLogTypeName[ucIdx],
                   (1==ucLogSwitch)?"On":"Off");
            return;
        }
    }

    OmciLogHelp();
}

1.2.8 掩碼錶

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

該例實現中用到訊息、掩碼、函式指標等概念。

2 程式設計思想

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

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

分離原則:策略同機制分離,介面同引擎分離

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

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

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

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

即使最簡單的程式邏輯讓人類來驗證也很困難,但就算是很複雜的資料,對人類來說,還是相對容易推導和建模的。資料比程式設計邏輯更容易駕馭。在複雜資料和複雜程式碼中選擇,寧可選擇前者。更進一步,在設計中,應該主動將程式碼的複雜度轉移到資料中去(參考“版本控制”)。

在“訊息處理”示例中,每個訊息處理的邏輯不變,但訊息可能是變化的。將容易變化的訊息和不容易變化的查詢邏輯分離,即“隔離變化”。此外,該例也體現訊息內部的處理邏輯(機制)和不同的訊息處理(策略)分離。

資料驅動程式設計可以應用於:

1.函式級設計,如本文示例。2.程式級設計,如用表驅動法實現狀態機。3.系統級設計,如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語言中使用函式指標時,一定要小心"型別陷阱"。