問題背景#

在 legacy code 中工作最困難的事情之一就是處理大型方法。很多時候可以用 Sprout MethodSprout Class 來避免直接修改大方法,但有時候你不得不面對它。

長方法(long method)已經很痛苦了,monster method 更甚。Monster method 是一種方法,它如此之長、如此複雜,以至於你根本不敢觸碰它。Monster method 可能長達數百甚至數千行,縮排層次深到幾乎無法追蹤。

當你遇到 monster method 時,你可能想把它印出來,鋪在走廊上,和同事一起研究。這不是玩笑——作者曾真的見過這種情形。


Varieties of Monsters#

Monster method 主要分為兩種類型(實際上很多方法是兩者的混合體):

Bulleted Methods#

Bulleted method 是一個幾乎沒有縮排的方法。它是一連串的程式碼區塊,看起來像是一個 bulleted list。如果幸運的話,區塊之間會有空行或註解來分隔。

特徵:

  • 方法本身不被縮排主導
  • 看起來像一系列順序執行的步驟
  • 理想情況下,每個區塊可以直接抽取為一個方法

Figure 22.1: Bulleted method

陷阱:

  • 區塊之間的分隔有時會騙人——臨時變數可能在一個區塊中宣告,在下一個區塊中使用
  • 拆分不像直接 copy/paste 那麼簡單

儘管如此,bulleted method 比另一種類型好處理,因為缺乏瘋狂的縮排讓我們更容易保持方向感。

Snarled Methods#

Snarled method 是一個被單一大型、深度縮排的區塊主導的方法。最簡單的情況是一個巨大的條件語句。

特徵:

  • 深度巢狀的 if/else、switch、迴圈
  • 當你嘗試對齊大括號時,會感到暈眩
  • 許多 snarl 內部藏有 bulleted section

Figure 22.2: Simple snarled method

Figure 22.3: Very snarled method

判斷方式:嘗試在長方法中對齊程式碼區塊的大括號。如果你開始感到暈眩(vertigo),那就是 snarled method。

Snarled method 之所以特別困難,是因為巢狀結構讓你很難為個別行為寫測試。條件深處的程式碼依賴大量的上下文設定。


Tackling Monsters with Automated Refactoring Support#

有 refactoring 工具(如 IDE 的 Extract Method)與否,會大幅影響處理 monster method 的策略。

有工具支援時#

如果工具能安全地抽取方法,你不需要測試來驗證抽取。工具會做分析,你只需要學會如何用抽取來把方法塑造成更好的形狀。

關鍵原則:

  • 只用工具做改動——避免任何手動編輯(包括重新排序語句、拆解表達式)
  • 如果工具支援 variable renaming,很好;如果不支援,延後再做
  • 這讓你在「已知安全的改動」和「可能不安全的改動」之間保持清楚的分界

在沒有測試的情況下做 automated refactoring 時,只使用工具。做完一系列自動化抽取後,你通常能建立測試來驗證後續的手動修改。

抽取的目標#

  1. 將邏輯與笨拙的依賴分離
  2. 引入 seam,讓後續更容易放入測試進行進一步重構

範例:CommoditySelectionPanel#

原始的 update 方法混合了 UI 操作和過濾邏輯:

class CommoditySelectionPanel {
    public void update() {
        if (commodities.size() > 0
                && commodities.GetSource().equals("local")) {
            listbox.clear();
            for (Iterator it = commodities.iterator();
                    it.hasNext(); ) {
                Commodity current = (Commodity)it.next();
                if (commodity.isTwilight()
                        && !commodity.match(broker))
                    listbox.add(commodity.getView());
            }
        }
    }
}

透過一系列 Extract Method,可以變成:

class CommoditySelectionPanel {
    public void update() {
        if (commoditiesAreReadyForUpdate()) {
            clearDisplay();
            updateCommodities();
        }
    }

    private boolean commoditiesAreReadyForUpdate() {
        return commodities.size() > 0
                && commodities.GetSource().equals("local");
    }

    private void clearDisplay() {
        listbox.clear();
    }

    private void updateCommodities() {
        for (Iterator it = commodities.iterator(); it.hasNext(); ) {
            Commodity current = (Commodity)it.next();
            if (singleBrokerCommodity(commodity)) {
                displayCommodity(current.getView());
            }
        }
    }

    private boolean singleBrokerCommodity(Commodity commodity) {
        return commodity.isTwilight() && !commodity.match(broker);
    }

    private void displayCommodity(CommodityView view) {
        listbox.add(view);
    }
}

抽取後的結構讓我們可以進一步分離出 CommodityFilter class,把過濾邏輯移出 panel。

Figure 22.4: Logic class extracted from CommoditySelectionPanel


The Manual Refactoring Challenge#

當沒有 automated refactoring 工具時,需要更保守。正確性完全取決於你自己和你能建立的測試。

手動抽取可能犯的錯誤#

  1. 忘記傳入變數——compiler 通常會告訴你,但如果 instance variable 恰好同名,可能不會報錯
  2. 新方法名稱隱藏或覆蓋了 base class 的方法
  3. 傳入參數或回傳值的型別搞錯

技巧一:Introduce Sensing Variable#

即使在重構 production code 時不想加功能,加一個感測變數(sensing variable) 來幫助驗證重構是可以的。

public class DOMBuilder {
    public boolean nodeAdded = false;  // sensing variable

    void processNode(XDOMNSnippet root, List childNodes) {
        // ...
        for (Iterator it = childNodes.iterator(); it.hasNext(); ) {
            XDOMNNode node = (XDOMNNode)it.next();
            if (node.type() == TF_G || node.type() == TF_H ||
                    (node.type() == TF_GLOT && node.isChild())) {
                paraList.add(node);
                nodeAdded = true;  // 設定 sensing variable
            }
        }
    }
}

寫測試驗證 sensing variable 是否被設定:

void testAddNodeOnBasicChild() {
    DOMBuilder builder = new DOMBuilder();
    List children = new ArrayList();
    children.add(new XDOMNNode(XDOMNNode.TF_G));
    builder.processNode(new XDOMNSnippet(), children);

    assertTrue(builder.nodeAdded);
}

有了這些測試,可以安心地抽取條件判斷:

private boolean isBasicChild(XDOMNNode node) {
    return node.type() == TF_G
        || node.type() == TF_H
        || node.type() == TF_GLOT && node.isChild();
}

使用 sensing variable 時,把它們保留到整個 refactoring session 結束。這樣你可以在過程中反覆使用這些測試來驗證抽取。完成後再移除 sensing variable,或將測試重構為直接測試抽取出的方法。

技巧二:Extract What You Know#

從小處開始——找到你有信心可以安全抽取的小片段(2-3 行),不需要測試就能確認正確。

關鍵指標:coupling count(耦合計數)——進出被抽取方法的值的數量。

void process(int a, int b, int c) {
    int maximum;
    if (a > b)
        maximum = a;
    else
        maximum = b;
    ...
}

抽取後:

void process(int a, int b, int c) {
    int maximum = max(a, b);
    ...
}

Coupling count = 3(兩個 input a, b,一個 output maximum)。

偏好 coupling count 小的抽取。Count 越小,犯錯的機率越低。理想情況下,coupling count 為 0(不需要任何 input/output)是最安全的。

技巧三:Gleaning Dependencies#

如果你想抽取的是方法中包含依賴的部分(純邏輯),但這些邏輯散落在方法各處,可以反過來做——抽取有依賴的部分到獨立方法中,留下純邏輯。

技巧四:Break Out a Method Object#

如果方法真的太大,所有其他方法都不夠用,可以把整個方法變成一個 class。這稱為 method object 重構:

  1. 建立一個新 class
  2. 在 constructor 中接受原方法的所有參數和原物件的參考
  3. 將方法本體複製到新 class 的一個方法中(如 run()
  4. Lean on the Compiler 修正所有存取問題

好處:方法成為 class 後,方法中的臨時變數變成 instance variable,你可以自由地在這個 class 中進行方法抽取而不用擔心 coupling count。


Strategy#

面對 monster method 的整體策略:

有 Refactoring 工具時#

  1. 使用自動化的 Extract Method 做粗略的分解
  2. 不要擔心抽取出的方法「不太適合」原 class——它們通常指向需要建立的新 class
  3. 建立測試後再進行手動調整

沒有 Refactoring 工具時#

  1. 使用 Introduce Sensing Variable 取得測試立足點
  2. Extract What You Know 安全地抽取小片段
  3. Gleaning Dependencies 分離邏輯和依賴
  4. 如果方法真的太大,使用 Break Out a Method Object

Skeletonize Methods#

對於 bulleted method,有兩種思考方式:

  • Skeletonize:抽取出方法本體中的邏輯區塊,留下骨架(控制流程)
  • Find Sequences:找出可以合併為一個方法呼叫的語句序列

這兩種方法殊途同歸——最終留下的都是高層級的控制流程,由一系列有意義名稱的方法呼叫組成。

處理 Snarled Method#

Snarled method 需要更多耐心:

  1. 先嘗試用 sensing variable 找到立足點
  2. 從最深處開始,逐步剝離條件內部的程式碼
  3. Extract What You Know 在每一層建立小的安全抽取
  4. 逐步 de-snarl 直到結構變得可管理

總結#

Monster method 看起來令人絕望,但它們是可以被馴服的。關鍵是:

  • 區分 bulleted 和 snarled——前者較好處理
  • 有工具就用工具——automated Extract Method 是最強大的武器
  • 沒工具就用保守技巧——sensing variable、extract what you know、gleaning dependencies
  • 最終手段是 method object——把方法變成 class,獲得更大的重構自由度
  • 永遠從小處開始——不要嘗試一次搞定整個 monster