數據驅動

數據驅動編程之表驅動法


前言:

最近在學習《Unix編程藝術》。以前粗略的翻過,以爲是介紹unix工具的。現在認真的看了下,原來是介紹設計原則的。它的核心就是第一章介紹的unix的哲學以及17個設計原則,而後面的內容就是圍繞它來展開的。以前說過,要學習適合自己的資料,而判斷是否適合的一個方法就是看你是否能夠讀得下去。我對這本書有一種相見恨晚的感覺。推薦有4~6年工作經驗的朋友可以讀一下。

正題:

作者在介紹Unix設計原則時,其中有一條爲“表示原則:把知識疊入數據以求邏輯質樸而健壯”。結合之前自己的一些經驗,我對這個原則很有共鳴,所以先學習了數據驅動編程相關的內容,這裏和大家分享出來和大家一起討論。

數據驅動編程的核心

數據驅動編程的核心出發點是相對於程序邏輯,人類更擅長於處理數據。數據比程序邏輯更容易駕馭,所以我們應該儘可能的將設計的複雜度從程序代碼轉移至數據。

真的是這樣嗎?讓我們來看一個示例。

假設有一個程序,需要處理其他程序發送的消息,消息類型是字符串,每個消息都需要一個函數進行處理。第一印象,我們可能會這樣處理: 

void msg_proc(const char *msg_type, const char *msg_buf) 
{ 
    if (0 == strcmp(msg_type, "inivite")) 
    { 
        inivite_fun(msg_buf); 
    } 
    else if (0 == strcmp(msg_type, "tring_100")) 
    { 
        tring_fun(msg_buf); 
    } 
    else if (0 == strcmp(msg_type, "ring_180")) 
    { 
        ring_180_fun(msg_buf); 
    } 
    else if (0 == strcmp(msg_type, "ring_181")) 
    { 
        ring_181_fun(msg_buf); 
    } 
    else if (0 == strcmp(msg_type, "ring_182")) 
    { 
        ring_182_fun(msg_buf); 
    } 
    else if (0 == strcmp(msg_type, "ring_183")) 
    { 
        ring_183_fun(msg_buf); 
    } 
    else if (0 == strcmp(msg_type, "ok_200")) 
    { 
        ok_200_fun(msg_buf); 
    }

    。。。。。。 
    else if (0 == strcmp(msg_type, "fail_486")) 
    { 
        fail_486_fun(msg_buf); 
    } 
    else 
    { 
        log("未識別的消息類型%s\n", msg_type); 
    } 
} 

上面的消息類型取自sip協議(不完全相同,sip協議借鑑了http協議),消息類型可能還會增加。看着常常的流程可能有點累,檢測一下中間某個消息有沒有處理也比較費勁,而且,沒增加一個消息,就要增加一個流程分支。

按照數據驅動編程的思路,可能會這樣設計: 

typedef void (*SIP_MSG_FUN)(const char *);

typedef struct __msg_fun_st 
{ 
    const char *msg_type;//消息類型 
    SIP_MSG_FUN fun_ptr;//函數指針 
}msg_fun_st;

msg_fun_st msg_flow[] = 
{ 
        {"inivite", inivite_fun}, 
        {"tring_100", tring_fun}, 
        {"ring_180", ring_180_fun}, 
        {"ring_181", ring_181_fun}, 
        {"ring_182", ring_182_fun}, 
        {"ring_183", ring_183_fun}, 
        {"ok_200", ok_200_fun},

        。。。。。。 
        {"fail_486", fail_486_fun} 
};

void msg_proc(const char *msg_type, const char *msg_buf) 
{ 
    int type_num = sizeof(msg_flow) / sizeof(msg_fun_st); 
    int i = 0;

    for (i = 0; i < type_num; i++) 
    { 
        if (0 == strcmp(msg_flow[i].msg_type, msg_type)) 
        { 
            msg_flow[i].fun_ptr(msg_buf); 
            return ; 
        } 
    } 
    log("未識別的消息類型%s\n", msg_type); 
} 

下面這種思路的優勢:

1、可讀性更強,消息處理流程一目瞭然。

2、更容易修改,要增加新的消息,只要修改數據即可,不需要修改流程。

3、重用,第一種方案的很多的else if其實只是消息類型和處理函數不同,但是邏輯是一樣的。下面的這種方案就是將這種相同的邏輯提取出來,而把容易發生變化的部分提到外面。

隱含在背後的思想

很多設計思路背後的原理其實都是相通的,隱含在數據驅動編程背後的實現思想包括:

1、控制複雜度。通過把程序邏輯的複雜度轉移到人類更容易處理的數據中來,從而達到控制複雜度的目標。

2、隔離變化。像上面的例子,每個消息處理的邏輯是不變的,但是消息可能是變化的,那就把容易變化的消息和不容易變化的邏輯分離。

3、機制和策略的分離。和第二點很像,本書中很多地方提到了機制和策略。上例中,我的理解,機制就是消息的處理邏輯,策略就是不同的消息處理(後面想專門寫一篇文章介紹下機制和策略)。

數據驅動編程可以用來做什麼:

如上例所示,它可以應用在函數級的設計中。

同時,它也可以應用在程序級的設計中,典型的比如用表驅動法實現一個狀態機(後面寫篇文章專門介紹)。

也可以用在系統級的設計中,比如DSL(這方面我經驗有些欠缺,目前不是非常確定)。

它不是什麼:

1、 它不是一個全新的編程模型:它只是一種設計思路,而且歷史悠久,在unix/linux社區應用很多;

2、它不同於面向對象設計中的數據:“數據驅動編程中,數據不但表示了某個對象的狀態,實際上還定義了程序的流程;OO看重的是封裝,而數據驅動編程看重的是編寫儘可能少的代碼。”

書中的值得思考的話:

數據壓倒一切。如果選擇了正確的數據結構並把一切組織的井井有條,正確的算法就不言自明。編程的核心是數據結構,而不是算法。——Rob Pike

程序員束手無策。。。。。只有跳脫代碼,直起腰,仔細思考數據纔是最好的行動。表達式編程的精髓。——Fred Brooks

數據比程序邏輯更易駕馭。儘可能把設計的複雜度從代碼轉移至數據是個好實踐。——《unix編程藝術》作者。

數據驅動編程之表驅動法

本文示例代碼採用的是c語言。
之前介紹過數據驅動編程《什麼是數據驅動編程》。裏面介紹了一個簡單的數據驅動手法。今天更進一步,介紹一個稍微複雜,更加實用的一點手法——表驅動法。
關於表驅動法,在《unix編程藝術》中有提到,更詳細的描述可以看一下《代碼大全》,有一章專門進行描述(大概是第八章)。

簡單的表驅動:
《什麼是數據驅動編程》中有一個代碼示例。它其實也可以看做是一種表驅動手法,只不過這個表相對比較簡單,它在收到消息後,根據消息類型確定使用調用什麼函數進行處理。

複雜一點的表驅動:

考慮一個消息(事件)驅動的系統,系統的某一模塊需要和其他的幾個模塊進行通信。它收到消息後,需要根據消息的發送方,消息的類型,自身的狀態,進行不同的處理。比較常見的一個做法是用三個級聯的switch分支實現通過硬編碼來實現:

switch(sendMode)  
{  
    case:  
}  
switch(msgEvent)  
{  
    case:  
}  
switch(myStatus)  
{  
    case:  
}  

這種方法的缺點:
1、可讀性不高:找一個消息的處理部分代碼需要跳轉多層代碼。
2、過多的switch分支,這其實也是一種重複代碼。他們都有共同的特性,還可以再進一步進行提煉。
3、可擴展性差:如果爲程序增加一種新的模塊的狀態,這可能要改變所有的消息處理的函數,非常的不方便,而且過程容易出錯。
4、程序缺少主心骨:缺少一個能夠提綱挈領的主幹,程序的主幹被淹沒在大量的代碼邏輯之中。

用表驅動法來實現:
根據定義的三個枚舉:模塊類型,消息類型,自身模塊狀態,定義一個函數跳轉表:

typedef struct  __EVENT_DRIVE  
{  
    MODE_TYPE mod;//消息的發送模塊  
    EVENT_TYPE event;//消息類型  
    STATUS_TYPE status;//自身狀態  
    EVENT_FUN eventfun;//此狀態下的處理函數指針  
}EVENT_DRIVE;  
  
EVENT_DRIVE eventdriver[] = //這就是一張表的定義,不一定是數據庫中的表。也可以使自己定義的一個結構體數組。  
{  
    {MODE_A, EVENT_a, STATUS_1, fun1}  
    {MODE_A, EVENT_a, STATUS_2, fun2}  
    {MODE_A, EVENT_a, STATUS_3, fun3}  
    {MODE_A, EVENT_b, STATUS_1, fun4}  
    {MODE_A, EVENT_b, STATUS_2, fun5}  
      
    {MODE_B, EVENT_a, STATUS_1, fun6}  
    {MODE_B, EVENT_a, STATUS_2, fun7}  
    {MODE_B, EVENT_a, STATUS_3, fun8}  
    {MODE_B, EVENT_b, STATUS_1, fun9}  
    {MODE_B, EVENT_b, STATUS_2, fun10}  
};  
  
int driversize = sizeof(eventdriver) / sizeof(EVENT_DRIVE)//驅動表的大小  
  
EVENT_FUN GetFunFromDriver(MODE_TYPE mod, EVENT_TYPE event, STATUS_TYPE status)//驅動表查找函數  
{  
    int i = 0;  
    for (i = 0; i < driversize; i ++)  
    {  
        if ((eventdriver[i].mod == mod) && (eventdriver[i].event == event) && (eventdriver[i].status == status))  
        {  
            return eventdriver[i].eventfun;  
        }  
    }  
    return NULL;  
}  


這種方法的好處:
1、提高了程序的可讀性。一個消息如何處理,只要看一下驅動表就知道,非常明顯。
2、減少了重複代碼。這種方法的代碼量肯定比第一種少。爲什麼?因爲它把一些重複的東西:switch分支處理進行了抽象,把其中公共的東西——根據三個元素查找處理方法抽象成了一個函數GetFunFromDriver外加一個驅動表。
3、可擴展性。注意這個函數指針,他的定義其實就是一種契約,類似於java中的接口,c++中的純虛函數,只有滿足這個條件(入參,返回值),纔可以作爲一個事件的處理函數。這個有一點插件結構的味道,你可以對這些插件進行方便替換,新增,刪除,從而改變程序的行爲。而這種改變,對事件處理函數的查找又是隔離的(也可以叫做隔離了變化)。、
4、程序有一個明顯的主幹。
5、降低了複雜度。通過把程序邏輯的複雜度轉移到人類更容易處理的數據中來,從而達到控制複雜度的目標。

繼承與組合
考慮一個事件驅動的模塊,這個模塊管理很多個用戶,每個用戶需要處理很多的事件。那麼,我們建立的驅動表就不是針對模塊了,而是針對用戶,應該是用戶在某狀態下,收到某模塊的某事件的處理。我們再假設用戶可以分爲不同的級別,每個級別對上面的提到的處理又不盡相同。
用面向對象的思路,我們可以考慮設計一個用戶的基類,實現相同事件的處理方法;根據級別不同,定義幾個不同的子類,繼承公共的處理,再分別實現不同的處理。這是最常見的一種思路,可以叫它繼承法。
如果用表驅動法怎麼實現?直接設計一個用戶的類,沒有子類,也沒有具體的事件的處理方法。它有一個成員,就是一個驅動表,它收到事件後,全部委託給這個驅動表去進行處理。針對用戶的級別不同,可以定義多個不同的驅動表來裝配不同的對象實例。這個可以叫他組合法。
繼承和組合在《設計模式》也有提到。組合的優勢在於它的可擴展性,彈性,強調封裝性。(繼承和組合可以參考這篇文章:面向對象之繼承組合淺談
至於這種情況下的驅動表,可以繼續使用結構體,也可以使用對象。

上面的方法的一點性能優化建議:
如果對性能要求不高,上面的方法足可以應付。如果性能要求很高,可以進行適當的優化。比如,可以建立一個多維數組,每一維分別表示模塊,狀態,消息。這樣,就可以根據這三者的枚舉直接根據下標定位到處理函數,而不是查表。(其實還是數據驅動的思想:數據結構是靜態的算法。)

數據驅動編程再更高級,更爲抽象一點的,應該就是流程腳本或者DSL了。我曾經寫過一個簡單的寄生在xml上的腳本來描述流程。這一塊後面抽時間介紹
轉自:http://www.cnblogs.com/chgaowei/archive/2011/11/14/2248920.html

根據以往的編程經驗,我覺得表驅動的方法十分方便,易於擴展,提高了開發效率,將新需求的編碼工作,交給數據格式的定義中。
1. 將邏輯的控制流程等都可以交給數據定義
2. 易於擴展,往往新添加一個數據屬性就能解決很多問題,程序的縱向流程,以及其他維度上的分支邏輯,在表驅動中都能橫向的屬性表示,變換的維度是橫向的,而且只有這一個維度。
3.組件思想,表中的每一列都是表的一個子集,組合關係。
4.思維方式的變更,新需求本身的邏輯處理已不再重要,透過具體的邏輯,分離出本質的數據不同,這時會發現,其實很多變更其實已經隱含在已經存在的數據格式中

當你定義了一張表,相當於你擁有了一副撲克,撲克相當於表中每一列,規則是怎樣全在於你在每一行添加數據。

缺點: 1. 空間的浪費。 這是最大的缺點
2. 時間的浪費,設計到表中查找已經表遍歷的問題,損失效率不大。
發佈了61 篇原創文章 · 獲贊 33 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章