---
# System prepended metadata

title: Working Effectively with Legacy Code (Chapter 1 ~ Chapter 6)
tags: [' Talk']

---

# Working Effectively with Legacy Code (Chapter 1 ~ Chapter 6)
> [name=謝朋諺]
> [time=Fri, Feb 16, 2024 9:51 PM]

---

> [TOC]

---

# Chapter 1 修改軟體
## 1.1 修改軟體的四個起因
### 1.1.1 添加特性和修正 bug
:::success
:book: 軟體原先以某種方式運作，現在用戶提出需求希望這個系統能夠做其他事情。
:book: 軟體原本行為有問題，導致需要修正或移除。
:::

### 1.1.2 改善設計
:::success
:book: 在不改變軟體行為的前提下，改善其設計的舉動稱之為重構 (Refactoring)
:::

重構時並不只是做低危險性的工作(如重整程式碼的格式)，或侵入性的危險工作(如重寫)程式碼區塊。

### 1.1.3 最佳化
:::success
:book: 與重構相同的地方是都會保持功能不變。
:book: 與重構不同的地方是最佳化專指對某些東西所用的資源優化，例如時間或記憶體。
:::

### 1.1.4 綜合起來
|          |  添加特性  | 修正 bug | 重構 | 最佳化 |
| -------- |:--------:|:--------:|:----:|:------:|
| 結構      |   改變    |   改變   | 改變  |   -    |
| 新功能    |   改變    |    -     |  -   |   -    |
| 功能      |    -     |   改變    |  -   |   -    |
| 資源使用   |    -     |    -     |  -   |  改變  |

## 1.2 危險的修改
為了減少風險，先考慮以下三個問題：
1. 我們要進行哪些修改?
2. 我們如何得知已經正確地完成了修改?
3. 我們如何得知沒有破壞任何(既有的)東西?

- 什麼樣風險是可以承擔的?
- 一直避免變動反而會導致生疏，因此應該更努力地去修改。

---

# Chapter 2 帶著回饋工作
系統變動有兩種:
- 編輯並祈禱(edit and pray)
- 覆蓋並修改(cover and modify)覆蓋並修改(cover and modify)
透過回歸測試 (regression testing) 週期性地執行測試來檢驗已知的良好行為，以便確診軟體是否還像以前那樣工作。
:::info
:bulb: **軟體夾鉗(Software Vise)**
當要動手進行變動的區域由測試包圍時，這些測試的作用就好比一把「軟體夾鉗」，透過這個軟體夾鉗就可以固定住軟體的大部分行為，只變動真正想變動的地方。
:::

## 2.1 什麼是單元測試
在單元測試這個領域，通常最關心的是一個最為「原子」的行為單位，在程式碼中指的就是「函數」，在物件導向的程式碼中指的就是類別。

大型測試有一些缺點：
- **錯誤定位**：測試離測試對象越遠，就越難確定測試失敗究竟意味著什麼。
- **執行時間**：大型測試往往需要更長時間來執行。
- **覆蓋**：在大型測試中，往往難以看出某段程式碼，與用來測試他的值之間的聯繫。

好的單元測試應有以下品質：
- **執行快**
- **能幫助我們定位問題所在**

:::warning
:fire: 一個需要耗時 **0.1 sec** 才能執行完的單元測試，已經算是一個慢的單元測試。
:::

:::danger
:scream: 有些測試容易跟單元測試混肴，例如以下就不是單元測試：
1. 與資料庫有互動
2. 進行了網路通訊
3. 接觸到檔案系統
4. 需要你對環境做特定的準備(如編輯設定檔案)才能夠執行。

以上這些測試也很重要，通常也會在單元測試控制工具來編寫他們。
但將他們跟真正的單元測試拆開來很重要，這樣就可以知道哪些測試是可以快速執行的！
:::

## 2.2 高層測試
覆蓋了應用程式的場景與互動的測試。

## 2.3 測試覆蓋
:::warning
:fire: 依賴性是軟體開發中最為關鍵的問題之一。在處理遺留程式碼的過程中有很大一部份工作都是圍繞著「解除依賴性以便使修改變得更容易」這個目標來進行的。
:::
:::warning
**遺留程式碼的困境**
我們在修改程式碼時，應當有測試在周圍「護」著。而為了將這些測試安置妥當，我們往往又得去修改程式碼。
:::
:::success
:computer: 當在遺留程式碼中解依賴時，你常常不得不暫時將自己的審美觀放在一旁。就像在動手術一樣總要有一個刀口，切完就會有一個疤痕。
但如果未來可以用測試覆蓋疤痕四周(即當初解依賴的點)，你就可以將疤痕抹去了。
:::

## 2.4 遺留程式碼修改演算法
以下演算法可以對遺留 code base 進行修改
1. 確定變動點
2. 找出測試點
3. 解依賴
4. 編寫測試
5. 修改、重構

後續章節會針對這幾個步驟做更詳細的技法介紹，這邊主要都介紹心法為主。

### 2.4.1 確定修改點
你所要進行修改的地點與程式碼的架構聯繫很緊密，前者敏感地依賴後者。

### 2.4.2 找出測試點
對於遺留程式碼來說找出測試點並不容易。

### 2.4.3 解依賴
**依賴往往是進行測試最明顯的障礙。**
1. 難以在測試控制工具中實例化目標物件。
2. 難以在測試控制工具中執行的方法

### 2.4.4 編寫測試
為遺留程式碼寫測試與為新程式寫測試是不同的事情。

### 2.4.5 變動和重構
作者提倡使用測試驅動 TDD 方式來替遺留程式碼添加特性。

---

# Chapter 3 感測和分離
有時候我們必須偽裝一些被影響的類別，來直接感測到他所受到的影響。

通常我們想要測試安置到位，有兩個理由去進行解依賴：感測和分離。
1. 感測：當我們無法存取到程式碼計算出的值時，需要透過解依賴來「感測」這些值。
2. 分離：當我們無法將一部份程式碼放入測試控制工具中去執行時，就需要解依賴將這塊程式碼「分離」出來。

## 3.1 偽裝成合作者 (Fake Collaborators)
:::success
:bulb: 用另外的程式碼來取代被依賴的類別並透過它來進行測試的話，就可以放手進行測試。
:::

### 3.1.1 偽物件 (Fake Object)
Sale Class
```java=
public class Sale
{
    private Display display;
    
    public Sale(Display display) {
        this.display = display;
    }
    
    public void scan(String barcode) {
        ...
        String itemLine = item.name()
            + " " + item.price().asDisplayText();
        display.showLine(itemLine);
        ...
    }
}
```

Interface:
```java=
// Java
public interface Display
{
    void showLine(String line);
}
```

Fake Display 的實作
```java=
// Java
public class FakeDisplay implements Display
{
    private String lastLine = "";
    
    public void showLine(String line) {
        lastLine = line;
    }
    
    public String getLastLine() {
    return lastLine;
    }
}
```

測試
```java=
// Java
import junit.framework.*;

public class SaleTest extends TestCase
{
    public void testDisplayAnItem() {
        FakeDisplay display = new FakeDisplay();
        Sale sale = new Sale(display);
        
        sale.scan("1");
        assertEquals("Milk $3.99", display.getLastLine());
    }
}
```

**Python 版本**
Sale Class
```python=
# python
class Sale:
    def __init__(self, display: father):
        self.display = display
    
    def scan(self, barcode):
        ...
        item = Item()
        item_line = f'{item.name()} {item.price().as_display_text()}'
        self.display.show_line(item_line)
        ...
```

ABC
```python=
# python
from abc import ABC, abstractmethod


class Display(ABC):
    @abstractmethod
    def show_line(self, line: str):
        pass
```

Fake Display 的實作
```python=
# python
class FakeDisplay(Display):
    def __init__(self):
        self.last_line = ''
    
    def show_line(self, line: str):
        self.last_line = line
    
    def get_last_line(self) -> str:
        return self.last_line
```

測試
```python=
# python
import unittest


class SaleTest(unittest.TestCase):
    def test_display_an_item(self):
        display = FakeDisplay()
        sale = Sale(display)
        
        sale.scan('1')
        self.assertEqual('Milk $3.99', display.get_last_line())

        
if __name__ == '__main__':
    unittest.main()
```

### 3.1.2 偽物件的兩面性
1. 一個是原本 interface 或是 abc 就有的 function `show_line`，而 `get_last_line` 則是為了我們測試寫的。
2. 對於 `Sale` 類別只知道 `showLine`，而且他會把 FakeDisplay 視為一個 `Display`。

### 3.1.3 偽物件手法的核心理念
在物件導向語言中就可以透過定義簡單的類別來實作；而非物件導向語言中，則可以定義一個替代函數來達到偽裝的目的。

### 3.1.4 仿物件(mock object)
:::success
:notebook: 仿物件就是內部進行斷言檢查的偽物件。
:::
製造一個假的 mock class
**Java 版本**
```java=
// java
import junit.framework.*;

public class SaleTest extends TestCase
{
    public void testDisplayAnItem() {
        MockDisplay display = new MockDisplay();
        display.setExpectation("showLine", "Milk $3.99");
        Sale sale = new Sale(display);
        sale.scan("1");
        display.verify();
    }
}
```

**Python 版本**
```python=
# python
import unittest
from unittest.mock import Mock


class SaleTest(unittest.TestCase):
    def test_display_an_item(self):
        display = Mock()
        display.show_line.return_value = 'Milk $3.99'
        
        sale = Sale(display)
        sale.scan('1')
        
        display.show_line.assert_called_once_with('Milk $3.99')

        
if __name__ == '__main__':
    unittest.main()
```

Mock 會直接告訴測試他期待的結果會是什麼，有點像是直接跳過 `display` 的測試，我們直接寫結果給他丟到 `Sale` 去做測試。

:::warning
:bulb: 偽物件和仿物件的語意有著極細微的差別。前者是「偽裝」成目標物件，目的是欺騙被測試者，讓他以為使用的是真正的目標物件。而後者雖說也是「假」貨，然而其目的卻在於盡量模仿真實的目標物件的行為，被測試者可以(從行為上)把它看作一個真正的目標物件來使用。
:::

# Chapter 4 接縫模型
一般而言程式設計語言對測試的支援不太好，要想最終得到易於測試的程式只有兩條路：
1. 一條路是邊開發邊寫測試
2. 另一條路是在前期花時間，試著將易測試性納入整體的設計考量。

人們對於第一種方案的期望甚高，不過若是以業界大量程式碼的驗證來看，第二種方法在過去並沒有取得多大的成功。

下面會介紹一些作者的怪癖給大家看。

## 4.1 一大段文字
作者小時候跑程式會算錢，所以會習慣把程式碼印出來確認沒問題再下去跑，就很像是在看一大串很長的文字。

另外他也提到：人們常說寫程式最好的方式是，將一小段可重用的程式組件組合再一起。但問題是這些組件被單獨重用的頻率並不算高。

## 4.2 接縫(seam)
:::info
:bulb: 接縫是指程式中的一些特殊點，在這些點上你無需做任何修改就可以達到變動程式行為的目的。
:::

**C++版本**
```cpp=
// C++
bool CAsyncSslRec::Init()
{
    if (m_bSslInitialized) {
        return true;
    }
    m_smutex.Unlock();
    m_nSslRefCount++;
    
    m_bSslInitialized = true;
    
    FreeLibrary(m_hSslDll1);
    m_hSslDll1 = 0;
    FreeLibrary(m_hSslDll2);
    m_hSslDll2 = 0;
    
    if (!m_bFailureSent) {
        m_bFailureSent = TRUE;
        PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
    }
    
    CreateLibrary(m_hSslDll1, "syncesel1.dll");
    CreateLibrary(m_hSslDll2, "syncesel2.dll");
    
    m_hSslDll1->Init();
    m_hSslDll2->Init();
    
    return true;
}
```
如果我想測試這段 code 但不想測到 `PostReceiveError` 這個函式，又不能更改程式碼的話可以透過增加一個間接層，來特別處理。

```cpp=
// C++
class CAsyncSslRec::PostReceiveError(UINT type. UINT errorcode)
{
    ::PostReceiveError(type. errorcode);
};
```
上面這段只是引入一個小小的間接層，但最終還是呼叫同樣的函數。

```cpp=
class TestingAsyncSslRec : public CAsyncSslRec
{
    virtual void PostReceiveError(UINT type. UINT errorcode) 
    {
    }
};
```
如果將 `CAsyncSslRec` 子類別化並覆寫 `PostReceiveError` 就可以改成上面這樣，這樣便可以跳過測試全域的 `PostReceiveError`。

**Python 版本**
```python=
# Python
class CAsyncSslRec:
    def __init__(self):
        self.m_bSslInitialized = False
        self.m_smutex = Smutex()  # Assuming Smutex is a class for mutex
        self.m_nSslRefCount = 0
        self.m_hSslDll1 = None
        self.m_hSslDll2 = None
        self.m_bFailureSent = False

    def Init(self):
        if self.m_bSslInitialized:
            return True
        
        self.m_smutex.Unlock()
        self.m_nSslRefCount += 1
        
        self.m_bSslInitialized = True
        
        FreeLibrary(self.m_hSslDll1)
        self.m_hSslDll1 = 0
        FreeLibrary(self.m_hSslDll2)
        self.m_hSslDll2 = 0
        
        if not self.m_bFailureSent:
            self.m_bFailureSent = True
            PostReceiveError(SOCKETCALLBACK, SSL_FAILURE)
        
        CreateLibrary(self.m_hSslDll1, "syncesel1.dll")
        CreateLibrary(self.m_hSslDll2, "syncesel2.dll")
        
        self.m_hSslDll1.Init()
        self.m_hSslDll2.Init()
        
        return True
```

Python 的話需要寫一個 monkey patching 蓋過他就可以
```python=
# python
import unittest
from unittest.mock import patch

class TestCAsyncSslRec(unittest.TestCase):
    @patch('main.PostReceiveError')
    def test_Init_no_PostReceiveError(self, mock_PostReceiveError):
        from main import CAsyncSslRec
        
        # 創建CAsyncSslRec實例
        ssl_rec = CAsyncSslRec()
        
        # 調用Init方法
        result = ssl_rec.Init()
        
        # 驗證是否成功初始化，並且未調用PostReceiveError
        self.assertTrue(result)
        self.assertFalse(mock_PostReceiveError.called)

if __name__ == '__main__':
    unittest.main()
```

## 4.3 接縫類型
### 4.3.1 預處理期接縫
- 有些程式語言可以透過編譯前做一些預處理，例如 C, C++，可以先由巨集預處理器進行預處理。
- 過度使用預處理並不是一個好主意，因為它會降低程式碼的清晰度，條件編譯指令(`#ifdef`, `#ifndef`, `#if`) 幾乎等於是在強迫你在同一份原始碼中維護多個不同的程式。
- 遇到一個接縫，就意味著我們可以改變其行為，我們也不能僅僅為了測試就修改程式碼，應該讓原始碼在產品階段和測試階段是完全一樣的。
- 每個接縫還有一個致能點(enabling point)。
:::info
:bulb: 致能點(enabling point)
每個接縫都有一個致能點，在這些點上你可以決定使用哪種行為。
:::

### 4.3.2 連接期接縫
- 連接期接縫有點像是 Java 的 import 會讓編譯器在幕後負責進行連接程序，當一個 Java 原始檔包含了 import 語句時就會檢查 import 的類別是否已被編譯，如果沒有，就先對其編譯，然後再檢查所有呼叫是否都能在執行期正確決議。

假設我們有一段 CAD 應用程式：
```java=
// java
void CrossPlaneFigure::rerender()
{
    //draw the label
    drawText(m_nX, m_nY, m_pchLabel, getClipLen());
    drawText(m_nX, m_nY, m_nX + getClipLen(), m_nY);
    drawText(m_nX, m_nY, m_nX, m_nY + getClipLen());
    if(!m_bShadowBox) {
        drawLine(m_nX + getClipLen(), m_nY,
                    m_nX + getClipLen(), m_nY + getDropLen());
        drawLine(m_nX, m_nY + getClipLen(),
                    m_nX + getClipLen(), m_nY + getDropLen());
    }
    
    //draw the figure
    for (int n = 0; n < edge.size(); n++) {
        ...
    }
    ...
    
}
```

假設我們想要測試這段，並且不想因為繪圖函式庫導致的問題來做測試，為了要取代所有的繪圖函式，就必須自己建立一個 `stub` 版本，用來連接這個應用程式的其他剩餘部分，這個 `stub` 裡面就可以全放上相應的空函數：
:::info
:bulb: **stub** 在程式設計師口中是指「一小段(可能是由某種工具自動生成的)程式碼(可能是二進制的)」用來佔據某個位置，以達到某個特定目的（如轉發，或這裡的行為消除等）
:::

```java=
// java
void drawText(int x, int y, char *text, int textLength)
{
}

void drawLine(int firstX, int firstY, int secodX, int secondY)
{
}
```

遇到帶有返回值的函數，你則需要在相應的 `stub` 函數裡面返回某些東西，通常可以返為一個代表成功的值，例如：
```java=
// java
int getStatus()
{
    return FLAG_OKAY;
}
```

第一種的方式是比較簡單的，但當需要回傳值的時候通常預設的回傳值在測試時都是不正確的，當然也可以直接用額外的資料結構實作額外的感測，例如：
```java=
// java
std::queue<GraphicsAction> actions;

void drawLine(int firstX, int firstY, int secondX, int secondY)
{
    actions.push_back(GraphicsAction(LINE_DRAW, firstX, firstY, secondX, secondY))
}
```

不過一般要做到額外的感測會導致事情變得更複雜，最好一開始就就選比較簡單的方案。

:::success
:notebook: 連接期接縫的致能點，始終都是位於程式碼之外！例如有時是在建構或部署腳本中，這就使得連接期接縫的使用，顯得不那麼醒目。
:::

### 4.3.3 物件接縫
透過物件導向方式來做接縫，以下是一個反例：
```java=
// java
// 現在有三個類別，其中 Cell 是父類別，另外兩個都是繼承它，
// 且三個都有實作 Recalculate() 這個 function
class Cell 
class ValueCell extends Cell
class FormulaCell extends Cell
```

```java=
// java
public class CustomSpreadsheet extends Spreadsheet
{
    public Spreadsheet buildMartSheet() {
        ...
        Cell cell = new FormulaCell(this, "A1", "=A2+A3");
        ...
        cell.Recalculate(); // 這樣我們測試時就沒辦法把這邊當物件接縫，因為已經被第 6 行給定義住了。
    }
}
```

但我們可以改成這樣：
```java=
public class CustomSpreadsheet extends Spreadsheet
{
    public Spreadsheet buildMartSheet(Cell cell) {
        ...
        cell.Recalculate(); // 這樣就可以透過參數來決定是用誰來做 Recalculate()
        ...
    }
}
```
這樣上面就是一個物件接縫，而他的致能點就是 `buildMartSheet` 的參數列表。

另一個靜態案例：
```java=
public class CustomSpreadsheet extends Spreadsheet
{
    public Spreadsheet buildMartSheet(Cell cell) {
        ...
        Recalculate(cell); // 這樣就可以透過參數來決定是用誰來做 Recalculate()
        ...
    }
    private static void Recalculate(Cell cell) {
    ...
    }
    ...
}
```

這邊 Recalculate 因為有 `static` 導致不能被繼承、不能被覆蓋；另外 `private` 也會導致無法繼承，因此可以改寫成這樣：
```java=
public class CustomSpreadsheet extends Spreadsheet
{
    public Spreadsheet buildMartSheet(Cell cell) {
        ...
        Recalculate(cell); // 這樣就可以透過參數來決定是用誰來做 Recalculate()
        ...
    }
    protected void Recalculate(Cell cell) {
    ...
    }
    ...
}

// 寫測試的時候就可以改寫
public class TestingCustomSpreadsheet extends CustomSpreadsheet
{
    protected void Recalculate(Cell cell) {
    ...
    }
    ...
}
```

:::success
:fire: 一般來說，如果你用的是物件導向語言，則**物件接縫**是最佳選擇。
- **預處理期接縫**以及**連接期接縫**某些時候是有用的，但他們沒有**物件接縫**那麼清楚明顯。
- 另外依賴另外兩種接縫可能會導致難以維護。
- 但如果程式到處瀰漫著依賴性，而且沒有更好的方案時，那可能更傾向於保留**預處理期接縫**和**連接期接縫**。
:::

# Chapter 5 工具
## 5.1 自動化重構工具
:::info
:bulb: 重構(Refactoring)
名詞。對軟體內部結構的一種調整，目的是在不改變軟體外在行為的前提下，提高其可理解性、降低其修改成本。
:::
慎選自動化重構工具，本書提出了幾點特徵可以拿來當作標準：
1. function 在同類別同名時，該重構工具是否能檢測出來？
2. 重構工具有沒有保留行為，沒有的話千萬不要用。

:::danger
:bomb: 如果一個工具它能替你完成重構，那麼我們會傾向於認為無須未待重構的程式碼編寫測試，但事情常常並非如此，以下就是個案例：
```java=
public class A {
    private int alpha = 0;
    private int getValue() {
        alpha++;
        return 12;
    }
    public void doSomething(){
        int v = getValue();
        int total = 0;
        for (int n = 0; n < 10; n++) {
            total += v;
        }
    }
}
```
而在重構工具自動化重構後，變成下面這樣：
```java=
public class A {
    private int alpha = 0;
    private int getValue() {
        alpha++;
        return 12;
    }
    public void doSomething(){
        int total = 0;
        for (int n = 0; n < 10; n++) {
            total += getValue();
        }
    }
}
```
**這邊的變數雖被移除了，卻把 alpha 值遞增了十次，而原先只是一次！** 這個變動明顯不能保留行為。
所以在自動化重構之前，先編寫必要的測試是有好處的。

當然也可以在沒有測試的時候做自動化重構，但就要先弄清楚你的工具會進行和不進行哪些檢查。
:::

## 5.2 仿物件
仿物件有很多免費套件可以使用，大部分可以透過 www.mockobjects.com 找到。（現在變成博弈網站了 XD，可以不用點他）

## 5.3 單元測試控制工具
- 作者極推 xUnit，但這好像是 .NET framework 的測試工具，包含 C#, F#, VB.NET 等語言所用。
- Java 的話則大推 JUnit。
- C++ 作者推薦自己寫的 CppUnitLite，他沒有瞧不起 CppUnit 原作者的意思，因為他就是 CppUnit 的作者 :sweat_smile: 

### 5.3.1 JUnit
介紹，略

### 5.3.2 CppUnitLie
介紹，略

### 5.3.3 NUnit 
NUmit 是 .NET 語言的測試框架之一，略

### 5.3.4 其他 xUnit 框架
略

## 5.4 一般測試控制工具
### 5.4.1 整合測試框架(FIT)
介紹 FIT 框架，主要是做整合測試，當你為你的系統編寫檔案，並在其中嵌入描述系統輸入和輸出的表格，並且可將這些檔案存成 HTML 的話，就可以將他們視為測試來執行。
http://fit.c2.com/

### 5.4.2 Fitness
另一個整合測試框架，是基於 Wiki 為宿主的 FIT，作者曾經也是其中開發的一員。

## 🧑‍💻 主委加碼：Python 的重構工具
### Bowler
Bowler 是一款用於在語法樹層級操作 Python 的重構工具。 它可以安全地大規模修改程式碼，同時確保產生的程式碼可以編譯和運行。 它提供了簡單的命令列介面和流暢的 Python API，可在程式碼中產生複雜的程式碼修改。

```python=
query = (
    Query([<file paths>])
    # rename class Foo to Bar
    .select_class("Foo")
    .rename("Bar")
    # change method buzz(x) to buzzard(x: int)
    .select_method("buzz")
    .rename("buzzard")
    .modify_argument("x", type_annotation="int")
)

query.diff()  # generate unified diff on stdout
query.write()  # write changes directly to files
```

### Sourcery
Sourcery 是一個自動程式碼審查程式，它將審查任何語言的任何拉取請求，以提供有關建議更改的即時回饋。每次審核都將包括變更摘要、高級回饋以及逐行建議/評論（如果相關）。

我們使用 ==OpenAI LLM== 進行程式碼審查。因此，我們需要向他們發送程式碼的各個部分（通常是 PR 的差異）。

![](https://raw.githubusercontent.com/sourcery-ai/sourcery-vscode/main/assets/Sourcery_VS_Code_Example.gif)

**Real-time refactoring suggestions**
![](https://raw.githubusercontent.com/sourcery-ai/sourcery-vscode/main/assets/Sourcery_VS_Code_Refactoring.gif)

**Continuous code quality feedback**
![](https://raw.githubusercontent.com/sourcery-ai/sourcery-vscode/main/assets/Sourcery_Metrics_VS_Code.gif)

# Chapter 6 時間緊迫，但必須修改
## 6.2 新生方法 (Sprout Method)
- 簡單來說就是用 function 取代原本寫在 code 裡面的判斷，以防止新添加的與原有的老程式碼之間，並沒有任何分界。
- 以下是將 ==日期發送== 跟 ==重複項目檢查== 混在一起的案例：
    ```python=
    # 只有日期發送
    class TransactionGate:
        def postEntries(self, entries):
            for entry in entries:
                entry.postDate()
            transactionBundle.getListManager().add(entries)
            ...
    ```
    ```python=
    from collections import deque

    class TransactionGate:
        def postEntries(self, entries):
            entriesToAdd = deque()
            for entry in entries:
                # 把判斷重複部分也寫進來了
                if not transactionBundle.getListManager().hasEntry(entry):
                    entry.postDate()
                    entriesToAdd.append(entry)
            transactionBundle.getListManager().add(entries)
        ...
    ```
- 這邊件事把移除重複項目視為一個完全獨立的操作，類似測試驅動開發法建立一個新的 function `uniqueEntries`:
    ```python=
    class TransactionGate:
        def uniqueEntries(self, entries):
            result = []
            for entry in entries:
                if not transactionBundle.getListManager().hasEntry(entry):
                    result.append(entry)
            return result
        ...
    ```
    再來原本的程式碼就只要呼叫他就好：
    ```python=
    class TransactionGate:
        ...
        def postEntries(self, entries):
            entriesToAdd = self.uniqueEntries(entries) # 呼叫判斷 function
            for entry in entries:
                entry.postDate()
            transactionBundle.getListManager().add(entriesToAdd)
    ```
- Sprout Method 步驟如下：
    1. 確定修改點。
    2. 如果你的修改可以在一個方法中的某一處，以單一連續序列語句出現，那麼在修改點插入一個方法呼叫，而被呼叫的就是我們下面要編寫的、用於完成有關工作的新方法，作者習慣先把這個呼叫先註解掉。
    3. 確定你需要原方法中的哪些局部變數，並將它們做為參數傳給新方法呼叫。
    4. 確定新訪法是否需要返回什麼值給原方法，如果需要的話就得相應修改對他的呼叫，使用一個變數來接收其返回值。
    5. 使用測試驅動的開發方法來開發新的方法。
    6. 使原方法中被註解掉的呼叫重新生效。

### 6.1.1 優點和缺點
- 缺點：
    - 使用它時，效果上等同於暫時放棄了原方法以及他所屬的類別，也就是你暫時不打算將他們置於測試之下和改善他們。
    - 程式碼會處於一個很尷尬的狀態，原方法可能包含了大量複雜的程式碼以及一個新生方法，為什麼這點小功能需要放到其他地方。
- 優點：
    - 新舊程式碼被清楚地隔開。
    - 即使沒辦法將舊程式碼置於測試之下，至少還能關注所要做的變動，並在新舊的程式碼之間建立清晰的介面。

## 6.2 新生類別
下面是新生類別的範例，主要功能是想透過 python 產出 html table 的報表：

```python=
class QuarterlyReportGenerator:
    def generate(self):
        results = database.queryResults(beginData, endData)
        pageText = "<html><head><title>Quarterly Report</title></head><body>\n"

        if len(results) != 0:
            for result in results:
                pageText += "<tr>"
                pageText += "<td>" + result.department + "</td>"
                pageText += "<td>" + result.manager + "</td>"
                pageText += "<td>" + str(result.netProfit // 100) + "</td>"
                pageText += "</tr>"
        else:
            pageText += "<p>No results for this period</p>"

        pageText += "</table></body></html>\n"
        return pageText
```

如果想要在 table 上面再加一個 header，就可以多寫一個 class：
```python=
class QuarterlyReportTableHeaderProducer:
    def makeHeader(self):
        return "<tr><td>Department</td><td>Manager</td>" \
               "<td>Profit</td><td>Expenses</td></tr>"
```

這樣就可以寫一個公共的介面(父類別)，讓這兩個 class 去繼承他：

```python=
class HTMLGenerator(ABC):
    @abstractmethod
    def generate(self):
        pass

class QuarterlyReportTableHeaderGenerator(HTMLGenerator):
    def generate(self):
        # 实现 QuarterlyReportTableHeaderGenerator 的 generate 方法
        pass

class QuarterlyReportGenerator(HTMLGenerator):
    def generate(self):
        # 实现 QuarterlyReportGenerator 的 generate 方法
        pass
```

- 一般有兩種情況會使用 Sprout Class:
    1. 所要進行的修改迫使你為某個類別添加一個全新的職責
    2. 想要添加的只是一個小小功能，可以把它放到原有的類別，但問題是無法將這個類別放入測試工具，所以只好另寫一個新的類別。
- Sprout Class 技術的步驟如下：
    1. 確定修改點。
    2. 如果你的修改可以在一個方法中的某一處，以單一連續序列語句出現，那麼在修改點插入一個類別來完成這些工作，並為這個類別取一個恰當的名字。然後在修改點插入程式碼，建立該類別的物件，並呼叫他的方法，然後將剛插入的這幾行程式碼註解掉。
    3. 確定你需要原方法中的哪些局部變數，並將它們做為參數傳給新類別的建構子。
    4. 確定新生類別是否需要返回什麼值給原方法，如果需要，則在該類別中提供一個相應的方法，並在原方法中插入對他的呼叫來獲得其返回值。
    5. 使用測試驅動的開發方法，來開發新類別。
    6. 使原方法中被註解掉的呼叫重新生效。

### 6.2.1 優點和缺點
- 優點：
    - 讓你在進行侵入性較強的修改時，有更大的自信去繼續進行自己的工作。
    - 在 C++ 中有額外的好處，可以不用調整任何已有得標頭檔。
- 缺點：
    - 系統中的概念複雜化，使用新生類別會破壞他的抽象化。