表驅動法 -《代碼大全》讀書筆記

表驅動法是一種編程模式,從表裏面查找信息而不是使用邏輯語句(if…else…switch),當是很簡單的狀況時,用邏輯語句很簡單,但若是邏輯很複雜,再使用邏輯語句就很麻煩了。java

好比查找一年中每月份的天數,若是用表驅動法,徹底不須要寫一堆if…else…語句,直接把每月份的天數存到一個數組裏就好了,取值的時候直接下標訪問,最多針對二月判斷一下閏年。這麼算的話,平時用的的HashMap,SparseArray也能夠算是表驅動android

表裏能夠存數據,也能夠存指令,或函數指針等均可以。數據庫

示例

看一個例子,計算保險的保險費率,不一樣年齡的人費率是不一樣的,當判斷費率時就要寫很長的if…else…來判斷不一樣的年齡段對應的費率,若是要區分性別,那麼判斷語句就會增長一倍,若是再判斷是否吸菸,是否已結婚,每個條件都會使判斷增長一倍。雖然能夠經過給是否已結婚,是否吸菸等設置一個比例係數,這樣就不用使判斷加倍,但這個係數可不能保證對不一樣年齡段或不一樣性別的人是相同的,並且若是要改變,就要很麻煩地改程序。編程

若是用表驅動法解決這個問題,直接拋棄邏輯判斷,使用一個表保存各個條件的費率,好比只考慮是性別,是否吸菸和年齡,就能夠定義一個三維數組保存各個條件的費率設計模式

double[][][] rate = {
        {{1,2,3}, {1,2,3}},
        {{1,2,3}, {1,2,3}}
};

for (int gender = 0; gender < rate.length; gender++) {
    System.out.println("Gender:" + gender);
    double[][] genderRate = rate[gender];
    for (int smoke = 0; smoke < genderRate.length; smoke++) {
        System.out.println("\tSmoke:" + smoke);
        double[] ageRate = genderRate[smoke];
        System.out.print("\t\tAge:");
        for (int age = 0; age < ageRate.length; age++) {
            System.out.print(ageRate[age] + " ");
        }
        System.out.println();
    }
}
// Output
Gender:0
    Smoke:0
        Age:1.0 2.0 3.0 
    Smoke:1
        Age:1.0 2.0 3.0 
Gender:1
    Smoke:0
        Age:1.0 2.0 3.0 
    Smoke:1
        Age:1.0 2.0 3.0
當想取一種狀況的費率時直接訪問數組就好了,好比封裝成下面的方法
private static double getRate(int gender, int smoke, int age) {
    return rate[gender][smoke][age];
}
前兩個條件性別和是否吸菸還好說,條件仍是相對固定的,對於年齡,不可能在數組中針對每一個年齡定義一個數據,由於通常都是按年齡段分的,這種狀況也能夠定義一個函數,把年齡轉換成一個數組中的第三維的索引。

使用表驅動法的好處是,當費率改變的時候,咱們用不着依次改每一個條件,直接修改這個費率表就好了,即便是新加條件,也只須要把這個費率表再加一維,修改一下根據條件獲取費率的函數就好了數組

還有另外一個好處就是,徹底能夠把這個數據表保存到文件中,在程序運行的時候讀取這個文件,這樣若是條件不變只是各個條件對應的數據變化時,直接修改這個文件就好了,甚至不用修改程序。緩存

另外一個複雜的例子是打印文件中存儲的信息數據結構

一個文件中存儲了不少信息,這些信息能夠分爲幾十種,每一個信息經過首部的一個信息ID分區app

若是用傳統的邏輯方法,步驟大概是:socket

  1. 讀取每條消息的ID
  2. 而後大量的if…else…或switch判斷消息類型
  3. 針對每種消息調用相應的處理函數

即便是面向對象的方法,也會爲每種消息定義一個類,這樣每添加一種消息都須要添加一個條件條件判斷或添加一個類。

那麼用表驅動法怎麼解決呢?

每條消息都由一些字段組成,這些字段是有限的,好比數字,字符串,布爾類型,日期等。咱們能夠用另一個文件記錄每一個消息對應的字段,以下所示

「Message Description」 
Field1 Float 「Prompt」 
Field2 Date 「Created Date」

每一種消息都經過上面的方式描述,全部消息類型被組織成一張表,這樣讀取消息的步驟就變爲了:

  • 讀取消息ID
  • 找到消息ID定義的消息描述
  • 讀取描述中的每個字段,根據字段的類型調用相應的打印方法

這樣就只須要爲每一個字段類型定義一個打印方法,全部的消息打印方法都是同樣的,除非增長字段的種類,不然即便添加消息類型也不須要修改代碼

表數據的訪問

表數據的訪問方法我不打算按《代碼大全》中的分類方法劃分爲:直接訪問、索引訪問與階梯訪問,這些只是針對特定的表的較優的訪問方法,像費率計算中的年齡條件,因爲年齡是分段的,但也只須要進行一個轉換,能夠說是直接訪問,根據年齡的區間能夠把年齡分爲不一樣的段,也能夠說是階段訪問。(能夠把各段的端點也保存到文件中增長靈活性)

像前面的計算每個月的天數的問題,直接以月份爲下標就能訪問須要的數據,或者像費率計算中的年齡條件,經過一個轉換就能夠取到須要的數據

還有的狀況是不方便直接訪問到數據的問的狀況是,好比你有100個商品,編號爲0-9999,這些編號是無規律的,你沒法根據商品編號獲取表鍵,若是直接用商品編號爲鍵,那就須要創建一個10000項的表,而其中只有100項有意義,若是商品相關的數據項很大會浪費不少空間,此時可使用索引技術,創建一個100項的表存儲商品,而後創建一個10000項的索引表存儲商品編號到商品表的表鍵的映射。這樣會浪費一個索引表,但所幸是索引表的表項通常很小,問題不大。

即便應用索引沒有節約空間而是浪費了空間,應用索引也可能會節約時間,好比查詢數據庫。

固然,上面的例子只是爲了說明狀況,若是數據不是存儲在文件中而是在內存中,HashMap就好了,不過若是數據結構很複雜,計算HashCode也須要時間,固然能夠優化HashCode的計算,如進行緩存等。還有稀疏矩陣等方法。

實際應用

表驅動法有沒有實際的應用?平時咱們確定或多或少想到了這個方法,只是就像設計模式同樣,大多數時候的思考只停留在當下的問題中,而沒有造成一個思想。當看到這個方法時我第一個想到的是Android啓動init時的init.rc。

如下是init.rc中zygote相關配置項:

service zygote /system/bin/app_process -Xzygote/system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd

像這段代碼中的writerestart關鍵字,咱們很容易看出這是一些指令,可是系統是怎麼將這些關鍵字對應到相應的指令上的?這就要看一個有意思的文件:keywords.h

#ifndef KEYWORD
int do_chroot(int nargs, char **args);
int do_chdir(int nargs, char **args);
...
int do_restart(int nargs, char **args);
...
int do_write(int nargs, char **args);
int do_copy(int nargs, char **args);
...
int do_wait(int nargs, char **args);
#define __MAKE_KEYWORD_ENUM__
#define KEYWORD(symbol, flags, nargs, func) K_##symbol,
enum {
    K_UNKNOWN,
#endif
    KEYWORD(capability,  OPTION,  0, 0)
    KEYWORD(chdir,       COMMAND, 1, do_chdir)
    ...
    KEYWORD(restart,     COMMAND, 1, do_restart)
    ...
    KEYWORD(write,       COMMAND, 2, do_write)
    KEYWORD(copy,        COMMAND, 2, do_copy)
    ...
    KEYWORD(ioprio,      OPTION,  0, 0)
#ifdef __MAKE_KEYWORD_ENUM__
    KEYWORD_COUNT,
};
#undef __MAKE_KEYWORD_ENUM__
#undef KEYWORD
#endif

使用keywords.h的地方在init_parser.c

#include "keywords.h"

#define KEYWORD(symbol, flags, nargs, func) \
    [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },

static struct {
    const char *name;
    int (*func)(int nargs, char **args);
    unsigned char nargs;
    unsigned char flags;
} keyword_info[KEYWORD_COUNT] = {
    [ K_UNKNOWN ] = { "unknown", 0, 0, 0 },
#include "keywords.h"
};
#undef KEYWORD
在init_parser.c中include了兩次keywords.h

第一次時尚未#defile KEYWORD,因此會定義do_restartdo_write等函數,而且在內部字義KEYWORD

#define KEYWORD(symbol, flags, nargs, func) K_##symbol,
這樣就會走這一句
enum {
    K_UNKNOWN,
一連串的KEYWORD定義會被這樣轉換
KEYWORD(restart,     COMMAND, 1, do_restart) -> K_restart,
最終的結果就是定義了一個enum:
enum {
    K_UNKNOWN,
    ...
    K_restart,
    ...
    K_write,
    ...
    KEYWORD_COUNT,
}
在第一次 #include "keywords.h" 後,init_parser.c中DEFINE了KEYWORD,並聲明瞭一個結構數組
#define KEYWORD(symbol, flags, nargs, func) \
    [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },

static struct {
    const char *name;
    int (*func)(int nargs, char **args);
    unsigned char nargs;
    unsigned char flags;
} keyword_info[KEYWORD_COUNT] = {
    [ K_UNKNOWN ] = { "unknown", 0, 0, 0 },
#include "keywords.h"
};
#undef KEYWORD
因爲此處定義了KEYWORD,因此再次 #include "keywords.h" #ifndef KEYWORD 內的語句就不會走,這一串KEYWORD宏被被這樣轉化:
KEYWORD(restart,     COMMAND, 1, do_restart) -> [ K_restart ] = { "restart", do_restart, 2, COMMAND },
其中COMMAND會也會被轉化,只是上面沒寫
#define SECTION 0x01
#define COMMAND 0x02
#define OPTION  0x04

這樣第二次include以後,就聲明瞭一個結構數組keyword_info,結構的成員分別是操做名、操做對應的處理函數、參數個數、操做類型。

經過對keywords.h的兩次include,init進程成功定義了一個枚舉和一張表,而且以枚舉爲鍵查找表能夠找到相應的處理函數,這樣就不用每次得到操做類型後查收處理函數了,直接讀表就好了,這就是前面說的,表中不僅能夠存數據,還能夠存指令、函數指針等。雖然從init.rc的命令名找到對應的枚舉名也須要查找,但從枚舉名處處理函數的查找方便多了。

相關文章
相關標籤/搜索