---
# System prepended metadata

title: Summery

---

## ch19 
## 對非物件導向的專案，如何安全地對它進行修改 My Project is not OO. How do I make safe changes?


#### Procedural languages 程序式語言 
* 這種語言中 UT 很難搞，不像物件導向一般，要打破依賴於程序式語言是很難的
* 該怎麼修改？最佳做法還是先寫測試，程序式以及 ＯＯ一樣通用
    * 找出 匯點（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) ，並寫測試
    *  **參數化建構子** 去動態綁定不同物件，實施不同行為

