數據驅動編程之表驅動法 本文示例代碼采用的是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上的腳本來描述流程。這一塊后面抽時間介紹。