## 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) ,並寫測試
* **參數化建構子** 去動態綁定不同物件,實施不同行為