# Form Template Method
兩個子類別中的方法用相似的步驟、相同的順序,但步驟不太一樣。
將步驟以同一種簽名(signatures)抽取到方法中來一般化(generalize),再提取(pull up)一般化的方法形成 Template Method 。
# 動機
Template Method (範本方法) 為實作一個演算法的不變的部份一次,再利用子類別來實作可變的行為 [DP]。在實作子類別的方法時,如果會變動的行為與不變的行為混雜在一起,那麼不變的行為會在子類別中重複。重構成 Template Method 可以幫助消除重複的不變行為。
一個 Template Method 不變行為包含以下:
- 被呼叫的方法以及這些方法的順序
- 子類別必須覆寫的抽象方法
- 子類別可能覆寫的 hook 方法(亦即:concrete methods)
例如:
public abstract class Game...
public void initialize() {
deck = createDeck();
shuffle(deck);
drawGameBoard();
dealCardsFrom(deck);
}
protected abstract Deck createDeck();
protected void shuffle(Deck deck) {
/// ...shuffle implementation
}
protected abstract void drawGameBoard();
protected abstract void dealCardsFrom(Deck deck);
2
3
4
5
6
7
8
9
10
11
12
13
在 initialize()
內被呼叫的方法的順序是固定不變的,子類別必須覆寫(override)的抽象方法也是不變的。但由 Game
提供實作的 shuffle()
並非不變:它是一個 hook,可以讓子類別繼承或是去覆寫。
Template Method 通常會呼叫 Factory Method,像是前面的 createDect()
。Introduce Polymorphic Creation with Factory Method 提供了一個實際的例子。
有些語言像是 Java 提供 final
可以把 Template Method 宣告成常數,避免子類別意外去覆寫。
優點
- 藉由移動固定的部份到超類別來移除子類別的重複碼。
- 簡化一個通用演算法的步驟。
- 讓子類別可以更容易來客製化一個演算法。
缺點
- 如果子類別必須實作演算法中的很多個方法,那麼這樣做會讓設計變得更複雜。
# 作法
- 在階層中找到 相似的方法:以相似的順序進行相似步驟的方法。在相似的方法上使用 Compose Method 來抽出相同的方法(在每個子類別中有相同簽名和內容的方法)和獨特的方法(在每個子類別中有不同簽名和內容的方法)。
在決定是否抽出相同和獨特的方法時,要考慮:如果抽出獨特方法,最後(5)需要在超類別內產生抽象或具體的版本。是否子類別繼承或是覆寫這個獨特方法有意義?如果沒意義,就抽出來放進相同的方法。 - 用 Pull Up Method [F] 相同的方法到超類別。
- 為了讓每個版本的相似方法中產生相同的內容,使用 Rename Method [F] 在每個獨特方法直到相似的方法在每個超類別都是相同的。
- 如果相似的方法在超類別中還沒有相同的簽名,使用 Rename Method [F] 來產生出相同的簽名。
- 使用 Pull Up Method [F] 在相似的方法(或是超類別內),為超類別內的每個獨特方法定義抽象方法。被 pulled up 的方法即是 Template Method。
# 範例
在 Replace Conditional Logic with Strategy 的範例最後,抽象類別 CapitalStrategy
底下有三個子類別:
這三個子類別正好包含少量的重複部份,可以用 Template Method 移除。比較常見的作法是結合 Strategy 和 Template Method 來產生具體 Strategy 類別,其中只有很少或是沒有重複碼。
CapitalStrategy
類別為了資本計算定義了一個抽象方法:
public abstract class CapitalStrategy...
public abstract double capital(Loan loan);
2
public class CapitalStrategyAdvisedLine {
public double capital(Loan loan) {
return loan.getCommitment() * loan.getUnusedPercentage() * duration(loan) * riskFactorFor(loan);
}
}
public class CapitalStrategyRevolver {
// ...
public double capital(Loan loan) {
return (loan.outstandingRiskAmount() * duration(loan) * riskFactorFor(loan)) + (loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan));
}
}
public class CapitalStrategyTermLoan {
// ...
public double capital(Loan loan) {
return loan.getCommitment() * duration(loan) * riskFactorFor(loan);
}
protected double duration(Loan loan) {
return weightedAverageDuration(loan);
}
private double weightedAverageDuration(Loan loan) {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
我們可以觀察到 CapitalStrategyAdvisedLine
對 CapitalStrategyTermLoan
來說是相同的,除了一個步驟 loan.getUnusedPercentage()
。
capital(...)
方法被CapitalStrategyAdvisedLine
和CapitalStrategyTermLoan
以相似方法實作。
使用 Compose Method 在capital(...)
的實作部份來抽出相同或獨特方法。
因為只有CapitalStrategyAdvisedLine
的loan.getUnusedPercentage()
部份不同、其他都相同,我們必須選擇把它抽成獨特方法或是把其他抽成相同方法,兩種方法都行得通。
根據 Domain Knowledge 可以知道公式:Risk Amount x Duration x Risk Factor
這引導我們去把CapitalStrategyAdvisedLine
loan.getCommitment() * loan.getUnusedPercentage()
抽出去變成riskAmountFor()
,同時處理CapitalStrategyTermLoan
相似的步驟:public class CapitalStrategyAdvisedLine { public double capital(Loan loan) { return riskAmountFor(loan) * duration(loan) * riskFactorFor(loan); } private double riskAmountFor(Loan loan) { return loan.getCommitment() * loan.getUnusedPercentage(); } } public class CapitalStrategyTermLoan { public double capital(Loan loan) { return riskAmountFor(loan) * duration(loan) * riskFactorFor(loan); } private double riskAmountFor(Loan loan) { return loan.getCommitment(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Domain-Driven Design 描述我們如何根據 domain knowledge 來引導我們選擇如何進行重構。
這一步是將相同的方法 pull up 到超類別。但是這裡我們抽出獨特的部份,因此略過這一步。
確保每個獨特方法
riskAmountFor
在子類別都有相同的簽名。確保相似的方法
capital
在子類別都有相同的簽名。現在
capital
在每個子類別都有相同的簽名,因此我們可以使出 Pull Up Method。public abstract class CapitalStrategy { // public abstract double capital(Loan loan); public double capital(Loan loan) { return riskAmountFor(loan) * duration(loan) * riskFactorFor(loan); } public abstract double riskAmountFor(Loan loan); }
1
2
3
4
5
6
7到目前已完成重構的工作。
以下是如果沒有
riskAmountFor()
會發生什麼事:public abstract class CapitalStrategy { public double capital(Loan loan) { return loan.getCommitment() * unusedPercentageFor(loan) * duration(loan) * riskFactorFor(loan); } public abstract double riskAmountFor(Loan loan); protected double unusedPercentageFor(Loan loan) { // hook method return 1.0; }; }
1
2
3
4
5
6
7
8
9public class CapitalStrategyAdvisedLine { protected double unusedPercentageFor(Loan loan) { return loan.getUnusedPercentage(); }; }
1
2
3
4
5public class CapitalStrategyTermLoan { // public double capital(Loan loan) { // return loan.getCommitment() * duration(loan) * riskFactorFor(loan); // } protected double duration(Loan loan) { return weightedAverageDuration(loan); } private double weightedAverageDuration(Loan loan) { // ... } }
1
2
3
4
5
6
7
8
9
10
11問題:
- 程式碼無法充分表達 risk-adjusted capital 公式
CapitalStretegy
的三個 subclasses 中的兩個:CapitalStrategyTermLoan
和CapitalStrategyRevolver
繼承了 hook 的無作為,因為那是CapitalStrategyAdvisedLine
獨特的步驟,唯它獨有。
現在我們看看
CapitalStrategyRevolver
如何利用新的capital()
Template Method。他原本的capital()
看起來像這樣:public class CapitalStrategyRevolver { public double capital(Loan loan) { return (loan.outstandingRiskAmount() * duration(loan) * riskFactorFor(loan)) + (loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan)); } }
1
2
3
4
5前半部份套公式。後半部份類似,但處理的是貸款未使用的部份。我們可以重構這段程式碼,讓它利用 Template Method,如下:
public class CapitalStrategyRevolver { public double capital(Loan loan) { return super.capital(loan) + (loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan)); } protected double riskAmountFor(Loan loan) { return loan.outstandingRiskAmount(); } }
1
2
3
4
5
6
7
8問題:這個新實作是否比前一份容易理解?作者認為確實如此,因為他清楚的告知資金的計算是依據通用公式和附加的未使用資金。我們可以使用 Extract Method 讓未使用資金更清楚一些:
public class CapitalStrategyRevolver { public double capital(Loan loan) { return super.capital(loan) + unusedCapital(loan); } public double unusedCapital(Loan loan) { return loan.unusedRiskAmount() * duration(loan) * unusedRiskFactor(loan); } }
1
2
3
4
5
6
7
8