重構(Refactoring)的目標#
接續上一章,當我們透過測試聞到重複(Duplication)的味道後,就要在測試保護下動手重構(Refactoring)。理想狀態下,重構幾乎不應該動到測試。
事前分析#
我們檢視原本長達七十幾行的 calculate 方法:
public int calculate(Transcript transcript) throws UnknownProgramTypeException {
String programType = transcript.getProgramType();
if (programType.equals("Bachelor")) {
List courses = transcript.getCourses();
if (courses.isEmpty()) return 0;
int total = courses.size();
int achieved = 0;
for (Course course : courses) {
if (course.getScore() >= 80) {
achieved++;
}
}
double rate = (double) achieved / total;
if (rate >= (double) 1 / 2) {
return 10_000;
} else if (rate >= (double) 1 / 3) {
return 5_000;
} else {
return 0;
}
}
if (programType.equals("Master")) {
List courses = transcript.getCourses();
if (courses.isEmpty()) return 0;
double totalCredit = 0.001D;
double totalWeightedScore = 0D;
for (Course course : courses) {
totalCredit += course.getCredit();
totalWeightedScore += course.getScore() * course.getCredit();
}
double weightedAverage = totalWeightedScore / totalCredit;
if (weightedAverage >= 90D) {
return 15_000;
} else if (weightedAverage >= 80D) {
return 7_500;
} else {
return 0;
}
}
// PhD 區塊類似,略
throw new UnknownProgramTypeException(programType);
}高階邏輯其實只是:
- 從成績單取得身份
- 依身份套用對應公式
- 都不是就丟錯誤
最大問題是太長,讀到後面就忘了前面,於是先處理「太長」這件事。
提取方法(Extract Method)#
使用 Martin Fowler 在 Refactoring 書中介紹的提取方法(Extract Method),把每段判斷後的細節包裝成方法:
public int calculate(Transcript transcript) throws UnknownProgramTypeException {
String programType = transcript.getProgramType();
if (programType.equals("Bachelor")) {
return calculateBachelor(transcript);
}
if (programType.equals("Master")) {
return calculateMaster(transcript);
}
if (programType.equals("PhD")) {
return calculatePhD(transcript);
}
throw new UnknownProgramTypeException(programType);
}請務必使用 IDE 的重構功能,不要手寫或複製貼上。每個機械式動作省 10 秒,累積起來會省下大量時間,這也是許多人覺得「重構很花時間」的真正元凶。
每一步重構後,都花幾十毫秒跑一次測試確認沒壞掉。
委託(Delegate)給專屬計算機#
接著處理「做太多事」。把每種身份的計算委託給對應的類別:
private final BachelorScholarshipCalculator bachelorScholarshipCalculator = new BachelorScholarshipCalculator();
private final MasterScholarshipCalculator masterScholarshipCalculator = new MasterScholarshipCalculator();
private final PhDScholarshipCalculator phDScholarshipCalculator = new PhDScholarshipCalculator();
public int calculate(Transcript transcript) throws UnknownProgramTypeException {
String programType = transcript.getProgramType();
if (programType.equals("Bachelor")) {
return bachelorScholarshipCalculator.calculateBachelor(transcript);
}
if (programType.equals("Master")) {
return masterScholarshipCalculator.calculateMaster(transcript);
}
if (programType.equals("PhD")) {
return phDScholarshipCalculator.calculatePhD(transcript);
}
throw new UnknownProgramTypeException(programType);
}未來修改任何身份的計算公式,都不需要動到 Service。
抽取統一介面#
為了符合開放封閉原則,再抽出共同 Calculator 介面:
private final Calculator bachelorScholarshipCalculator = new BachelorScholarshipCalculator();
private final Calculator masterScholarshipCalculator = new MasterScholarshipCalculator();
private final Calculator phDScholarshipCalculator = new PhDScholarshipCalculator();
public int calculate(Transcript transcript) throws UnknownProgramTypeException {
String programType = transcript.getProgramType();
if (programType.equals("Bachelor")) {
return bachelorScholarshipCalculator.calculate(transcript);
}
if (programType.equals("Master")) {
return masterScholarshipCalculator.calculate(transcript);
}
if (programType.equals("PhD")) {
return phDScholarshipCalculator.calculate(transcript);
}
throw new UnknownProgramTypeException(programType);
}重構每一步都不要走太大步。每一步都會「暫時」讓程式暴露在風險之中,所以盡量小步前進,萬一出事必須中止重構,也只要丟棄一點點就好。
改寫成符合高階抽象的形式#
最後再進一步改寫,把「找計算機」與「執行計算機」分離:
public int calculate(Transcript transcript) throws UnknownProgramTypeException {
Calculator calculator = findCalculator(transcript.getProgramType());
return calculator.calculate(transcript);
}
private Calculator findCalculator(String programType) throws UnknownProgramTypeException {
switch (programType) {
case "Bachelor":
return new BachelorScholarshipCalculator();
case "Master":
return new MasterScholarshipCalculator();
case "PhD":
return new PhDScholarshipCalculator();
default:
throw new UnknownProgramTypeException(programType);
}
}calculate 方法現在只做流程協調。新增學生身份、修改任一計算公式、修改金額,都不再需要動 calculate,這個方法只會因為「流程改變」這一個原因而被修改。
整個過程中測試完全沒改動,因為對外行為從未變化。這才是重構的真正樣貌。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10262382