---
# System prepended metadata

title: 第 30 章-設計模式
tags: [Kent Beck的測試驅動開發：案例導向的逐步解決之道讀書會]

---

---
title: 第 30 章-設計模式
tags: Kent Beck的測試驅動開發：案例導向的逐步解決之道讀書會
---

# 第 30 章-設計模式

我們處理的問題大部分是來自於使用的工具（內部），而不是需求（外部）。因此就算需求不同，還是可以找到某些常見的共同問題的解決方案。這就是模式。

透過物件組織常見、可預測的設計方式，解決內部產生的子問題。設計模式的成功證明了物件導向設計的開發人員所看到的通用性。

這本書的例子有使用到這些設計：

### 命令（P181）

把被呼叫的方法用物件呈現，而非訊息溝通（這裡指的是呼叫方法傳遞的參數）。建立一個物件表示要做的事情，並在建立的同時把執行所需的訊息都準備好，接著就可以用共同的介面直接呼叫了！（像是本書提到的 run 或是 execute）

[[Day 9] 初探設計模式 - 命令模式 ( Command Pattern )](https://ithelp.ithome.com.tw/articles/10204425)

### 數值物件（P182）

當建立後狀態就不會再被改變的物件。在 Ch3 有使用到，關於這個物件所有的操作都會回傳一個新的物件，避免別名問題（不同名稱但是指同個物件），導致原本的物件在各種操作後不經意地被改變。

### 空物件（P183）

表示一個物件的基本型態。建立另外一個例外的物件做特殊情況的操作，例如書中舉的例子：

```javascript=
const setReadOnly = () => {
  const guard = System.getSecurityManager();
  if (guard) guard.canWrite(path);
  return System.setReadOnly(this);
}
```

在每一次取得 SecurityManager 物件的時候都要另外判斷 guard，每取一次就要再判斷一次，為了這種特殊情況可以再另外建立一個類別 LaxSecurity，讓在 guard 為 null 時呼叫 canWrite 不會出錯：

```javascript=
class LaxSecurity {
  canWrite(path) {}
}
```

之後把判斷改寫在 getSecurityManager 裡面：

```javascript=
static getSecurityManager() {
  return security ? security : new LaxSecurity();
}
```

如此一來外面使用 getSecurityManage 後就不用再判斷 guard 了：

```javascript=
const setReadOnly = () => {
  const guard = System.getSecurityManager();
  guard.canWrite(path);
  return System.setReadOnly(this);
}
```

### 範本方法（P185）

在父類別定義不變的流程，透過繼承實作具體的內容。在程式開發領域有幾種經典流程：

* 輸入、處理、輸出
* 送出訊息、接收方回應
* 讀取命令、回傳結果

或是像之前實作的 TestCase 就有 setUp、runTest、tearDown 這樣子的流程。

範本方法通常會在撰寫完實作內容後才設計，當兩個子類別有某個不同的實作內容時，將同樣的部分拉到父類別，並消除子類別中的重複，就是範本方法了。而不是一開始就先設計好「我的步驟就是要這樣子」，避免將來使用 inline 條件式去處理不同的地方，再提取到子類別。

現實的例子：React 的生命週期（class component）

### 插入式物件（P186）

將兩種以上的變化封裝在實作同個抽象的不同物件，提供一致的抽象操作。一般會用條件判斷的方式表達變化，但如果你在某個地方使用條件判斷，這個條件判斷可能也會出現在其他地方。

以書上的例子來說，他現在在做的介面會根據滑鼠所點的地方，來決定接下來執行的行為要做什麼（滑動或放開滑鼠）：

```javascript=
class SelectionTool {
  constructor() {
    this.selected;
  }
  
  mouseDown() {
    const selected = findFingure();
    if (selected) this.selected = selected;
  }
  
  mouseMove() {
    if (this.selected) {
      move(selected);
    } else {
      moveSelectionRectangle();
    }
  }
  
  mouseUp() {
    if (!this.selected) return selectAll();
  }
}
```

面對這種狀況可以用插入式物件，建立有相同介面的兩種物件，在 mouseDown 的時候將特定操作的物件插入到 mode 中，之後就消除重複的散落的判斷式，直接執行 mode：

```javascript=
class SelectionTool {
  constructor() {
    this.mode;
  }
  
  mouseDown() {
    const selected = findFingure();
    if (selected) {
      this.mode = SingleSelection(selected);
    } else {
      this.mode = MultipleSelection();
    }
  }
  
  mouseMove() {
    this.mode.move();
  }
  
  mouseUp() {
    this.mode.selectAll();
  }
}
```

### 插入式選擇器（P188）

動態呼叫不同實體的不同方法，避免建立不必要的子類別。如果每個不同實作的子類別都只有一個方法的實作不同，那多建立子類別實在是大材小用，這時候就撰寫一個包含 switch 方法和各種實作的類別，直接呼叫就行了：

```javascript=
class Report {
  constructor() {
    this.printMessage;
  }

  report(printMessage) {
    this.printMessage = printMessage;
  }
  
  print() {
    switch(this.printMessage) {
      case 'printHTML':
        this.printHTML();
        break;
      case 'printXML':
        this.printXML();
        break;
    }
  }
  
  printHTML() {}

  printXML() {}
}
```

接著只要有新的 print 方法就在 Report 裡建立該方法的實作，然後在 print 的 switch 中加入對應的方法名稱就好。另外也可以用 reflection 的方式動態呼叫方法，就省去寫 switch 了：

```javascript=
class Report {
  constructor() {
    this.printMessage;
  }

  report(printMessage) {
    this.printMessage = printMessage;
  }
  
  print() {
    this[this.printMessage]();
  }
  
  printHTML() {}

  printXML() {}
}
```

現實的例子：Git 的 config

### 工廠方法（P189）

透過呼叫方法建立物件的實體，而不是建構式以增加建立物件的靈活性。在 Ch8 中有使用這個方法將建立 Frane 和 Dollar 物件移到 Money 中。

### 冒名頂替（P190）

在已存在的協議中，引入新的實作物件來引入變化。去實作另外一個物件擁有的所有介面，並把原本的物件改成新的物件。剛剛提到的空物件，和待會會講到的遞迴組合都是冒名頂替的例子。

### 遞迴組合（P192）

使用一個物件來表示一群物件行為的組合。以 Transaction 和 Account 為例子，Account 裡面的 balance 會根據 Transaction 的 value 算出有多少餘額：

```javascript=
class Transaction {
  constructor(value) {
    this.value = value;
  }
}

class Account {
  constructor() {
    this.transactions = [];
  }
  
  balance() {
    return this.trnasactions.reduce(
      (result, transaction) => result + transaction.value,
      0,
    );
  }
}
```

那如果要變成計算所有 Account 的餘額呢？就不是只以 Transaction 為單位，也有可能是 Account，這時候可以讓 Transaction 和 Account 都用相同的介面來取得 balance：

```javascript=
class Transaction {
  constructor(value) {
    this.value = value;
  }
  
  balance() {
    return this.value;
  }
}
```

Account 裡面用來 reduce 的名稱也改成 holdings：

```javascript=
class Account {
  constructor() {
    this.holdings = [];
  }
  
  balance() {
    return this.holdings.reduce(
      (result, holding) => result + transaction.balance(),
      0,
    );
  }
}
```

這麼一來不論是 hodings 裡面是 Transaction 或 Account 都沒差，而 Account 也可以成為一個容納很多 Account 的物件。

### 收集參數（P194）

透過傳遞一個參數物件收集不同物件之間呼叫方法的結果，最後再用它來做事。在 Ch21 用來記錄執行了幾個測試和失敗幾個的計數，就是利用這個收集參數的模式。

### 單例模式（P195）

如果你的語言沒有全域變數的概念，那就不要使用。
