天天看點

資料驅動程式設計之表驅動法

本文示例代碼采用的是c語言。

之前介紹過資料驅動程式設計

《什麼是資料驅動程式設計》

。裡面介紹了一個簡單的資料驅動手法。今天更進一步,介紹一個稍微複雜,更加實用的一點手法——表驅動法。

關于表驅動法,在《unix程式設計藝術》中有提到,更詳細的描述可以看一下《代碼大全》,有一章專門進行描述(大概是第八章)。

簡單的表驅動:

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

複雜一點的表驅動:

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

view plain

  1. switch(sendMode)  
  2. {  
  3.     case:  
  4. }  
  5. switch(msgEvent)  
  6. switch(myStatus)  

這種方法的缺點:

1、可讀性不高:找一個消息的處理部分代碼需要跳轉多層代碼。

2、過多的switch分支,這其實也是一種重複代碼。他們都有共同的特性,還可以再進一步進行提煉。

3、可擴充性差:如果為程式增加一種新的子產品的狀态,這可能要改變所有的消息處理的函數,非常的不友善,而且過程容易出錯。

4、程式缺少主心骨:缺少一個能夠提綱挈領的主幹,程式的主幹被淹沒在大量的代碼邏輯之中。

用表驅動法來實作:

根據定義的三個枚舉:子產品類型,消息類型,自身子產品狀态,定義一個函數跳轉表:

  1. typedef struct  __EVENT_DRIVE  
  2.     MODE_TYPE mod;//消息的發送子產品  
  3.     EVENT_TYPE event;//消息類型  
  4.     STATUS_TYPE status;//自身狀态  
  5.     EVENT_FUN eventfun;//此狀态下的處理函數指針  
  6. }EVENT_DRIVE;  
  7. EVENT_DRIVE eventdriver[] = //這就是一張表的定義,不一定是資料庫中的表。也可以使自己定義的一個結構體數組。  
  8.     {MODE_A, EVENT_a, STATUS_1, fun1}  
  9.     {MODE_A, EVENT_a, STATUS_2, fun2}  
  10.     {MODE_A, EVENT_a, STATUS_3, fun3}  
  11.     {MODE_A, EVENT_b, STATUS_1, fun4}  
  12.     {MODE_A, EVENT_b, STATUS_2, fun5}  
  13.     {MODE_B, EVENT_a, STATUS_1, fun6}  
  14.     {MODE_B, EVENT_a, STATUS_2, fun7}  
  15.     {MODE_B, EVENT_a, STATUS_3, fun8}  
  16.     {MODE_B, EVENT_b, STATUS_1, fun9}  
  17.     {MODE_B, EVENT_b, STATUS_2, fun10}  
  18. };  
  19. int driversize = sizeof(eventdriver) / sizeof(EVENT_DRIVE)//驅動表的大小  
  20. EVENT_FUN GetFunFromDriver(MODE_TYPE mod, EVENT_TYPE event, STATUS_TYPE status)//驅動表查找函數  
  21.     int i = 0;  
  22.     for (i = 0; i < driversize; i ++)  
  23.     {  
  24.         if ((eventdriver[i].mod == mod) && (eventdriver[i].event == event) && (eventdriver[i].status == status))  
  25.         {  
  26.             return eventdriver[i].eventfun;  
  27.         }  
  28.     }  
  29.     return NULL;  

這種方法的好處:

1、提高了程式的可讀性。一個消息如何處理,隻要看一下驅動表就知道,非常明顯。

2、減少了重複代碼。這種方法的代碼量肯定比第一種少。為什麼?因為它把一些重複的東西:switch分支處理進行了抽象,把其中公共的東西——根據三個元素查找處理方法抽象成了一個函數GetFunFromDriver外加一個驅動表。

3、可擴充性。注意這個函數指針,他的定義其實就是一種契約,類似于java中的接口,c++中的純虛函數,隻有滿足這個條件(入參,傳回值),才可以作為一個事件的處理函數。這個有一點插件結構的味道,你可以對這些插件進行友善替換,新增,删除,進而改變程式的行為。而這種改變,對事件處理函數的查找又是隔離的(也可以叫做隔離了變化)。、

4、程式有一個明顯的主幹。

5、降低了複雜度。通過把程式邏輯的複雜度轉移到人類更容易處理的資料中來,進而達到控制複雜度的目标。

繼承與組合

考慮一個事件驅動的子產品,這個子產品管理很多個使用者,每個使用者需要處理很多的事件。那麼,我們建立的驅動表就不是針對子產品了,而是針對使用者,應該是使用者在某狀态下,收到某子產品的某事件的處理。我們再假設使用者可以分為不同的級别,每個級别對上面的提到的處理又不盡相同。

用面向對象的思路,我們可以考慮設計一個使用者的基類,實作相同僚件的處理方法;根據級别不同,定義幾個不同的子類,繼承公共的處理,再分别實作不同的處理。這是最常見的一種思路,可以叫它繼承法。

如果用表驅動法怎麼實作?直接設計一個使用者的類,沒有子類,也沒有具體的事件的處理方法。它有一個成員,就是一個驅動表,它收到事件後,全部委托給這個驅動表去進行處理。針對使用者的級别不同,可以定義多個不同的驅動表來裝配不同的對象執行個體。這個可以叫他組合法。

繼承群組合在《設計模式》也有提到。組合的優勢在于它的可擴充性,彈性,強調封裝性。(繼承群組合可以參考這篇文章:

面向對象之繼承組合淺談

至于這種情況下的驅動表,可以繼續使用結構體,也可以使用對象。

上面的方法的一點性能優化建議:

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

資料驅動程式設計再更進階,更為抽象一點的,應該就是流程腳本或者DSL了。我曾經寫過一個簡單的寄生在xml上的腳本來描述流程。這一塊後面抽時間介紹。

繼續閱讀