## ch19 ## 對非物件導向的專案,如何安全地對它進行修改 My Project is not OO. How do I make safe changes? #### Procedural languages 程序式語言 * 這種語言中 UT 很難搞,不像物件導向一般,要打破依賴於程序式語言是很難的 * 該怎麼修改?最佳做法還是先寫測試,程序式以及 OO一樣通用 * 找出 匯點(pinch point) * 結構上的主要通道,針對少數地方寫測試,即可達到驗證多數其他方法 * 連接期接縫 (link seams) * 利用偽函式去消除不必要的副作用 * 預處理接縫(preprocessing seams)(如果語言有支援巨集) * C 語言的巨集,在編譯時替換成偽函式 ### 19.1 一個簡單的案例 An Easy Case ```csharp= void set_writetime(struct buffer_head * buf, int flag) { int newtime; if (buffer_dirty(buf)) { /* Move buffer to dirty list if jiffies is clear */ newtime = jiffies + (flag ? bdf_prm.b_un.age_super : bdf_prm.b_un.age_buffer); if(!buf->b_flushtime || buf->b_flushtime > newtime) buf->b_flushtime = newtime; } else { buf->b_flushtime = 0; } } ``` * 上述程式碼,設定 jiffies 以及傳遞 buffer_head 結構即可做測試。 * buffer_head 結構(型別/物件)的 buf * jiffies -> timer 變數 * 但是,常常會是函式裡又呼叫另一個函式,或者呼叫第三方函數,衍生出一些 side effect。 ### 19.2 一個棘手的案例 An Hard Case ```csharp= #include "ksrlib.h" int scan_packets(struct rnode_packet *packet, int flag) { struct rnode_packet *current = packet; int scan_result, err = 0; while(current) { scan_result = loc_scan(current->body, flag); if(scan_result & INVALID_PORT) { ksr_notify(scan_result, current); } ... current = current->next; } return err; } ``` * 上述程式碼,ksr_notify 函式中,後面的參數 current 會有副作用。所以可以用 **連接期接縫(link seams)** 方式去除其副作用,簡單來說就是寫一個 ksr_notify 偽函式,但是裡面什麼也不做。專注測 scan_packets,其他行為將其釘牢。 但是,如果今天就是要測試改變 ksr 函式的返回值,就別用 link seams 因為會很麻煩。 比如今天有 n 種測試,可能就會衍生出 ksr 中 n 種不行的行為,且要寫很多判斷來跑各種行為。無奈的是,在程序式語言中,就只能這樣做。 * **預處理接縫(preprocessing seam)** C 的巨集,簡化函數編寫的測試, ```csharp= #include "ksrlib.h" #ifdef TESTING #define ksr_notify(code, packet) // define 宣告巨集 #endif int scan_packets(struct rnode_packet *packet, int flag) { struct rnode_packet *current = packet; int scan_result, err = 0; while(current) { scan_result = loc_scan(current->body, flag); if(scan_result & INVALID_PORT) { ksr_notify(scan_result, current); } ... current = current->next; } return err; } #ifdef TESTING #include <assert.h> int main () { struct rnode_packet packet; packet.body = ... ... int err = scan_packets(&packet, DUP_SCAN); assert(err & INVALID_PORT); ... return 0; } #endif ``` 上述程式碼,當 TESTING 成立時,編譯時期程式中的 ksr_notify 會被替換空。 * 呈上述程式碼,善用 **include** 語法把測試或產品程式碼檔案分離,比較乾淨。(中 p.251|英 p.235) * 略過一堆程式碼...(中 p.252|英 p.235, 236) * 總而言之,利用巨集,將測試檔案一個一個包起來,可以每個測試案例包成函式,方便擴充及觀看(相對全部寫在同個檔案)。 ### 19.3 添加新行為 Adding New Behavior * 引入新函數,比在舊函式中添加行為好,至少可以對新函式寫測試 * TDD 可以幫助程序式語言中解依賴,先對函式寫測試,再加入原有程式碼 ```csharp= void send_command(int id, char *name, char *command_string) { char *message, *header; if (id == KEY_TRUM) { message = ralloc(sizeof(int) + HEADER_LEN + ... ... } else { ... } sprintf(message, "%s%s%s", header, command_string, footer); mart_key_send(message); // 送訊息出去 free(message); } ``` 把上面程式碼 mart_key_send 前(2 ~ 9 lines)的 code 抽成函式 form_command,並寫個測試,如下: ```csharp= char *command = form_command(1, "Mike Ratledge", "56:78:cusp-:78"); assert(!strcmp("<-rsp-Mike Ratledge><56:78:cusp-:78><-rspr>", command)); ``` ```csharp= char *form_command(int id, char *name, char *command_string) { char *message, *header; if (id == KEY_TRUM) { message = ralloc(sizeof(int) + HEADER_LEN + ... ... } else { ... } sprintf(message, "%s%s%s", header, command_string, footer); return message; } ``` 即可將 send_command 改寫成 ```csharp= void send_command(int id, char *name, char *command_string) { char *command = form_command(id, name, command_string); mart_key_send(command); free(message); } ``` 以上做法,將非依賴邏輯抽成函式,可以降低一些依賴問題。 * 那如果遇到到處都是外部呼叫的情況呢?以 **貸款利息** 的例子如下: ```csharp= void calculate_loan_interest(struct temper_loan *loan, int calc_type) { ... db_retrieve(loan->id); ... db_retrieve(loan->lender_id); ... db_update(loan->id, loan->record); ... loan->interest = ... } ``` * C 的 **函數指標 (function pointer)** 可以幫助測試。在測試時把指標指向偽函式;在正式產品時指向真正資料庫存取函數。 ```csharp= // 建立包含一系列函數指標的結構體 struct database { void (*retrieve)(struct record_id id); void (*update)(struct record_id id, struct record_set *record); ... }; // 可以這樣用(舊) extern struct database db; (*db.update)(load->id, loan->record); // 也可以這樣用(如果編譯器是新的) extern struct database db; db.update(load->id, loan->record); ``` ### 19.4 利用物件導向優勢 Taking Advantage of Object Orientation * 物件接縫(Object Seam) (中 p.46| 英 p.40) * 利用 inject 方式將欲使用的物件注入,有種接縫的意思 * 接縫的特質 * 程式碼中容易辨認 * 拆分成更小,更易理解的區塊 * 更好的靈活性(測試、擴張系統為例) ```javascript= public Speardsheet buildMartSheet(Cell cell){ ... // Cell cell = new XXX() // 反例,這樣就不是接縫了 cell.Recalculate(); // cell 是 inject 進來的 ... } ``` * 把 C 編譯成 C++,因為 C++ 有支援物件導向 * 以下是一連串把 C 轉換成物件導向的code... ```csharp= int scan_packets(struct rnode_packet *packet, int flag) { struct rnode_packet *current = packet; int scan_result, err = 0; while(current) { scan_result = loc_scan(current->body, flag); if(scan_result & INVALID_PORT) { ksr_notify(scan_result, current); // 主要是希望測試時,此函數不要真正發 notify } ... current = current->next; } return err; } ``` 將 ksr_notify 抽出來放進 ResultNotifier 類別,其中 (line 3) virtual 表示可以執行時多型 ```csharp= class ResultNotifier { public: virtual void ksr_notify(int scan_result, struct rnode_packet *packet); }; ``` 引入新原始檔案,並將預設實作放在該檔案,保持變數名稱的參數一致,降低錯誤風險 ```csharp= extern "C" void ksr_notify(int scan_result, struct rnode_packet *packet); void ResultNotifier::ksr_notify(int scan_result, struct rnode_packet *packet) { ::ksr_notify(scan_result, packet); // ::表示實作該函式 } ``` 將 globalResultNotifier.ksr_notify 取代掉原有的 ksr_notify (中 p.258 | 英 p.241) ```csharp= #include "ksrlib.h" extern ResultNotifier globalResultNotifier; int scan_packets(struct rnode_packet *packet, int flag) { struct rnode_packet *current = packet; int scan_result, err = 0; while(current) { scan_result = loc_scan(current->body, flag); if(scan_result & INVALID_PORT) { globalResultNotifier.ksr_notify(scan_result, current); } ... current = current->next; } return err; } ``` 再進一步,把 scan_packets 包進一個全域參照 ```csharp= class Scanner { public: int scan_packets(struct rnode_packet *packet, int flag); }; ``` 即可利用 **參數化建構子 (p.395) Parameterize Constructor (p.379)** 達到動態綁定及多型。 ```csharp= class Scanner { private: ResultNotifier& notifier; public: Scanner(); Scanner(ResultNotifier& notifier); int scan_packets(struct rnode_packet *packet, int flag); }; // in the source file Scanner::Scanner() : notifier(globalResultNotifier) {} Scanner::Scanner(ResultNotifier& notifier) : notifier(notifier) {} ``` 將 notifier 物件 inject 進去,比較好做測試 (中 p.258 259 | 英 p.242) ### 19.5 一切都是物件導向 It’s All Object Oriented * 所有程序式語言其實都是物件導向 * code...把一堆函數一一抽出來,最後弄成一個 programm.main() (略) (中 p.259 | 英 p.243) * 總而言之,老式 C 語言程式就是個大物件,封裝成全域參照,則是在建立新物件,將系統切成一個個子部件,進而一一測試。 * 除了抽取依賴抽成函式外,還能做些什麼? * 去把 OO 寫更好,包成類別及抽出方法,去做職責分離 * 詳細去看 CH20 # Summery * 本章講了程序式語言如何拆解依賴、添加行為並做測試 * 大函式裡面抽成一個一個小函式 * 利用 **連接期接縫** (mock 偽函式) 或 **預處理接縫** (C 語言的巨集)讓程式碼可以好測試 * C 語言的函數指標可以指向需要依賴的物件,做到正式與測試物件分離 * 程序性語言如果能轉換成物件導向最好,比較好用 OO 方式做測試 * 利用 **物件接縫** (injection) ,並寫測試 * **參數化建構子** 去動態綁定不同物件,實施不同行為