Refactoring(重構)教學手冊
📚 目錄
- 2.1 紅燈-綠燈-重構循環
- 2.2 重構的黃金法則
- 2.3 重構的時機
- 2.4 安全重構的步驟
- 2.5 實務注意事項
- 3.1 什麼是程式碼壞味道?
- 3.2 常見的程式碼壞味道
- 3.2.1 過長方法(Long Method)
- 3.2.2 過多參數(Long Parameter List)
- 3.2.3 重複程式碼(Duplicated Code)
- 3.2.4 過大類別(Large Class)
- 3.2.5 壞味道的量化指標
- 3.3 壞味道識別工具
- 3.4 實務練習
- 4.1 方法層級重構
- 4.2 類別層級重構
- 4.2.1 Extract Class(提取類別)
- 4.2.2 Move Method(搬移方法)
- 4.3 條件邏輯重構
- 4.4 重構方法選擇流程
- 4.5 實務練習
- 5.1 重構的安全網:單元測試
- 5.2 測試先行的重構策略
- 5.3 TDD 與重構的結合
- 5.4 重構時的測試最佳實務
- 5.5 重構測試檢查清單
1. 重構基本概念
1.1 什麼是重構?
重構(Refactoring) 是在不改變程式碼外部行為的前提下,改善程式碼內部結構的過程。
graph TD
A[原始程式碼] --> B[重構過程]
B --> C[改進後的程式碼]
A --> D[外部行為]
C --> D
style A fill:#ffcccc
style C fill:#ccffcc
style D fill:#ccccff1.2 重構的目標
- 提高程式碼可讀性:讓程式碼更容易理解
- 降低維護成本:減少未來修改的複雜度
- 提升設計品質:改善程式架構
- 增強可測試性:讓程式更容易進行單元測試
1.3 重構 vs 重寫
| 項目 | 重構 | 重寫 |
|---|---|---|
| 外部行為 | 保持不變 | 可能改變 |
| 風險 | 低 | 高 |
| 時間 | 較短 | 較長 |
| 測試 | 現有測試繼續有效 | 需要新的測試 |
1.4 實務案例
重構前:
public class Calculator {
public double calc(double a, double b, String op) {
if (op.equals("+")) {
return a + b;
} else if (op.equals("-")) {
return a - b;
} else if (op.equals("*")) {
return a * b;
} else if (op.equals("/")) {
return a / b;
}
return 0;
}
}重構後:
public class Calculator {
public double add(double a, double b) {
return a + b;
}
public double subtract(double a, double b) {
return a - b;
}
public double multiply(double a, double b) {
return a * b;
}
public double divide(double a, double b) {
if (b == 0) {
throw new IllegalArgumentException("除數不能為零");
}
return a / b;
}
}2. 重構的基本原則
2.1 紅燈-綠燈-重構循環
graph LR
A[紅燈:寫失敗測試] --> B[綠燈:寫最少程式碼讓測試通過]
B --> C[重構:改善程式碼品質]
C --> A
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ffffcc2.2 重構的黃金法則
- 保持功能不變:外部行為必須完全相同
- 小步快跑:每次只做小幅度的改變
- 頻繁測試:每次修改後立即執行測試
- 版本控制:每次重構後提交程式碼
2.3 重構的時機
🟢 適合重構的時機
- 三法則(Rule of Three):當同樣的程式碼出現第三次時
- 增加新功能前:先清理程式碼再添加功能
- 修復錯誤時:理解程式碼後進行改善
- 程式碼審查時:發現可改善的地方
🔴 不適合重構的時機
- 接近截止日期:時間壓力下容易出錯
- 程式碼無法正常運作:先修復功能再重構
- 沒有測試覆蓋:缺乏安全網
2.4 安全重構的步驟
flowchart TD
A[1. 檢查測試覆蓋率] --> B{測試覆蓋率足夠?}
B -->|否| C[補充測試]
B -->|是| D[2. 執行一次完整測試]
C --> D
D --> E{所有測試通過?}
E -->|否| F[修復問題]
E -->|是| G[3. 進行小幅度重構]
F --> D
G --> H[4. 立即執行測試]
H --> I{測試通過?}
I -->|否| J[回復變更]
I -->|是| K[5. 提交變更]
J --> G
K --> L[繼續下一個重構]2.5 實務注意事項
- 建立重構檢查點:重構前先提交一次乾淨的版本
- 使用 IDE 重構工具:善用自動重構功能降低出錯機率
- 團隊溝通:重構可能影響其他開發者,需要適時溝通
- 效能監控:重構後確認效能沒有明顯降低
3. 識別壞味道(Code Smells)
3.1 什麼是程式碼壞味道?
程式碼壞味道是程式中可能存在設計問題的徵兆,雖然程式功能正常,但結構不佳。
mindmap
root((Code Smells))
方法層級
過長方法
過多參數
重複程式碼
大量註解
類別層級
過大類別
資料類別
功能羨慕
拒絕繼承
系統層級
發散式變化
霸王類別
中間人
不當親密關係3.2 常見的程式碼壞味道
3.2.1 過長方法(Long Method)
症狀: 方法超過 20-30 行,難以理解其用途
範例:
// 壞味道:過長方法
public void processOrder(Order order) {
// 驗證訂單 (10 行)
if (order == null) throw new IllegalArgumentException("訂單不能為空");
if (order.getItems().isEmpty()) throw new IllegalArgumentException("訂單項目不能為空");
// ... 更多驗證邏輯
// 計算總價 (15 行)
double total = 0;
for (OrderItem item : order.getItems()) {
double itemPrice = item.getPrice() * item.getQuantity();
if (item.getDiscount() > 0) {
itemPrice = itemPrice * (1 - item.getDiscount());
}
total += itemPrice;
}
// ... 更多計算邏輯
// 更新庫存 (20 行)
// ... 庫存更新邏輯
// 發送通知 (10 行)
// ... 通知邏輯
}重構建議: 使用 Extract Method
3.2.2 過多參數(Long Parameter List)
症狀: 方法參數超過 3-4 個
範例:
// 壞味道:過多參數
public void createUser(String firstName, String lastName, String email,
String phone, String address, String city,
String country, int age, boolean isActive) {
// 實作邏輯
}重構建議: 使用 Introduce Parameter Object
3.2.3 重複程式碼(Duplicated Code)
症狀: 相同或相似的程式碼出現在多個地方
範例:
// 壞味道:重複程式碼
public double calculateSalary(Employee employee) {
double baseSalary = employee.getBaseSalary();
double bonus = baseSalary * 0.1;
double tax = (baseSalary + bonus) * 0.2;
return baseSalary + bonus - tax;
}
public double calculatePension(Employee employee) {
double baseSalary = employee.getBaseSalary();
double bonus = baseSalary * 0.1;
double totalIncome = baseSalary + bonus;
return totalIncome * 0.05;
}重構建議: 提取公用方法
3.2.4 過大類別(Large Class)
症狀: 類別行數過多(通常超過 200-300 行)或負責太多職責
檢測方法:
// 壞味道:過大類別
public class UserManager {
// 使用者 CRUD 操作
public void createUser() { /* ... */ }
public void updateUser() { /* ... */ }
public void deleteUser() { /* ... */ }
// 使用者驗證
public boolean validateEmail() { /* ... */ }
public boolean validatePassword() { /* ... */ }
// 郵件發送
public void sendWelcomeEmail() { /* ... */ }
public void sendResetPasswordEmail() { /* ... */ }
// 報表生成
public void generateUserReport() { /* ... */ }
public void exportUserData() { /* ... */ }
// ... 更多不相關的方法
}重構建議: 使用 Extract Class
3.2.5 壞味道的量化指標
為了更客觀地評估程式碼品質,可以使用以下量化指標:
方法複雜度指標:
| 指標 | 良好 | 可接受 | 需要重構 |
|---|---|---|---|
| 方法行數 | ≤ 20 | 21-50 | > 50 |
| 參數個數 | ≤ 3 | 4-5 | > 5 |
| 巢狀深度 | ≤ 3 | 4 | > 4 |
| 循環複雜度 | ≤ 10 | 11-20 | > 20 |
類別複雜度指標:
| 指標 | 良好 | 可接受 | 需要重構 |
|---|---|---|---|
| 類別行數 | ≤ 200 | 201-500 | > 500 |
| 方法個數 | ≤ 20 | 21-30 | > 30 |
| 欄位個數 | ≤ 10 | 11-15 | > 15 |
| 內聚度 | > 0.7 | 0.5-0.7 | < 0.5 |
程式碼重複指標:
// 使用工具檢測重複程式碼
// PMD 配置範例
<rule ref="category/java/design.xml/CyclomaticComplexity">
<properties>
<property name="classReportLevel" value="80"/>
<property name="methodReportLevel" value="10"/>
</properties>
</rule>3.3 壞味道識別工具
3.3.1 靜態分析工具
- SonarQube:程式碼品質分析
- Checkstyle:Java 程式碼風格檢查
- PMD:程式碼缺陷檢測
3.3.2 IDE 內建工具
- IntelliJ IDEA:程式碼檢查功能
- Eclipse:PMD、Checkstyle 插件
- VS Code:SonarLint 擴展
3.4 實務練習
請檢視以下程式碼,識別其中的壞味道:
public class OrderProcessor {
public String processOrder(String customerName, String customerEmail,
String customerPhone, String customerAddress,
String productName, double productPrice,
int quantity, String discountCode) {
// 驗證客戶資訊
if (customerName == null || customerName.trim().isEmpty()) {
return "錯誤:客戶姓名不能為空";
}
if (customerEmail == null || !customerEmail.contains("@")) {
return "錯誤:電子郵件格式不正確";
}
// 計算總價
double total = productPrice * quantity;
if (discountCode != null) {
if (discountCode.equals("SAVE10")) {
total = total * 0.9;
} else if (discountCode.equals("SAVE20")) {
total = total * 0.8;
}
}
// 發送確認郵件
String emailContent = "親愛的 " + customerName + ",您的訂單已確認...";
// ... 郵件發送邏輯
return "訂單處理成功,總金額:" + total;
}
}識別到的壞味道:
- 過多參數(8個參數)
- 過長方法(包含多種職責)
- 字串型別的回傳值處理錯誤訊息
- 硬編碼的折扣邏輯
4. 常見重構方法
4.1 方法層級重構
4.1.1 Extract Method(提取方法)
用途: 將長方法拆分成多個小方法
重構前:
public void printOwing() {
printBanner();
// 計算未付金額
double outstanding = 0.0;
for (Order order : orders) {
outstanding += order.getAmount();
}
// 印出詳細資訊
System.out.println("客戶名稱:" + name);
System.out.println("未付金額:" + outstanding);
}重構後:
public void printOwing() {
printBanner();
double outstanding = calculateOutstanding();
printDetails(outstanding);
}
private double calculateOutstanding() {
double outstanding = 0.0;
for (Order order : orders) {
outstanding += order.getAmount();
}
return outstanding;
}
private void printDetails(double outstanding) {
System.out.println("客戶名稱:" + name);
System.out.println("未付金額:" + outstanding);
}4.1.2 Rename Variable(重新命名變數)
用途: 使變數名稱更具表達性
重構前:
public double calculateTotal(List<Item> items) {
double t = 0;
for (Item i : items) {
double p = i.getPrice();
double q = i.getQuantity();
double d = i.getDiscount();
t += p * q * (1 - d);
}
return t;
}重構後:
public double calculateTotal(List<Item> items) {
double totalAmount = 0;
for (Item item : items) {
double price = item.getPrice();
double quantity = item.getQuantity();
double discount = item.getDiscount();
totalAmount += price * quantity * (1 - discount);
}
return totalAmount;
}4.1.3 Introduce Parameter Object(引入參數物件)
用途: 將多個參數組合成一個物件
重構前:
public void createUser(String firstName, String lastName, String email,
String phone, String address, int age) {
// 建立使用者邏輯
}重構後:
public class UserInfo {
private final String firstName;
private final String lastName;
private final String email;
private final String phone;
private final String address;
private final int age;
public UserInfo(String firstName, String lastName, String email,
String phone, String address, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone = phone;
this.address = address;
this.age = age;
}
// getters...
}
public void createUser(UserInfo userInfo) {
// 建立使用者邏輯
}4.1.4 Replace Method with Method Object(以方法物件取代方法)
用途: 當方法有很多局部變數,難以進行 Extract Method 時
重構前:
public class OrderCalculator {
public double calculateTotal(Order order) {
double basePrice = order.getQuantity() * order.getItemPrice();
double discountFactor = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
double discountLevel2 = Math.min(discountFactor, order.getItemPrice() * 100);
double shipping;
if (basePrice > 1000) {
shipping = 0;
} else {
shipping = basePrice * 0.1;
}
return basePrice - discountLevel2 + shipping;
}
}重構後:
public class OrderCalculator {
public double calculateTotal(Order order) {
return new OrderTotalCalculation(order).calculate();
}
}
class OrderTotalCalculation {
private final Order order;
private double basePrice;
private double discountFactor;
private double discountLevel2;
private double shipping;
public OrderTotalCalculation(Order order) {
this.order = order;
}
public double calculate() {
calculateBasePrice();
calculateDiscount();
calculateShipping();
return basePrice - discountLevel2 + shipping;
}
private void calculateBasePrice() {
basePrice = order.getQuantity() * order.getItemPrice();
}
private void calculateDiscount() {
discountFactor = Math.max(0, order.getQuantity() - 500) * order.getItemPrice() * 0.05;
discountLevel2 = Math.min(discountFactor, order.getItemPrice() * 100);
}
private void calculateShipping() {
if (basePrice > 1000) {
shipping = 0;
} else {
shipping = basePrice * 0.1;
}
}
}4.2 類別層級重構
4.2.1 Extract Class(提取類別)
用途: 將一個類別分解成兩個類別
重構前:
public class Person {
private String name;
private String telephoneNumber;
private String areaCode;
public String getName() { return name; }
public String getTelephoneNumber() { return telephoneNumber; }
public String getAreaCode() { return areaCode; }
public void setName(String name) { this.name = name; }
public void setTelephoneNumber(String telephoneNumber) {
this.telephoneNumber = telephoneNumber;
}
public void setAreaCode(String areaCode) { this.areaCode = areaCode; }
}重構後:
public class Person {
private String name;
private TelephoneNumber telephoneNumber;
public String getName() { return name; }
public TelephoneNumber getTelephoneNumber() { return telephoneNumber; }
public void setName(String name) { this.name = name; }
public void setTelephoneNumber(TelephoneNumber telephoneNumber) {
this.telephoneNumber = telephoneNumber;
}
}
public class TelephoneNumber {
private String number;
private String areaCode;
public String getNumber() { return number; }
public String getAreaCode() { return areaCode; }
public void setNumber(String number) { this.number = number; }
public void setAreaCode(String areaCode) { this.areaCode = areaCode; }
public String getFullNumber() {
return "(" + areaCode + ") " + number;
}
}4.2.2 Move Method(搬移方法)
用途: 將方法移動到更適合的類別
重構前:
public class Account {
private AccountType type;
private int daysOverdrawn;
public double overdraftCharge() {
if (type.isPremium()) {
double result = 10;
if (daysOverdrawn > 7) {
result += (daysOverdrawn - 7) * 0.85;
}
return result;
} else {
return daysOverdrawn * 1.75;
}
}
}重構後:
public class Account {
private AccountType type;
private int daysOverdrawn;
public double overdraftCharge() {
return type.overdraftCharge(daysOverdrawn);
}
}
public class AccountType {
public double overdraftCharge(int daysOverdrawn) {
if (isPremium()) {
double result = 10;
if (daysOverdrawn > 7) {
result += (daysOverdrawn - 7) * 0.85;
}
return result;
} else {
return daysOverdrawn * 1.75;
}
}
public boolean isPremium() {
// 實作邏輯
}
}4.3 條件邏輯重構
4.3.1 Replace Conditional with Polymorphism(以多型取代條件式)
重構前:
public class Bird {
private BirdType type;
public double getSpeed() {
switch (type) {
case EUROPEAN_SWALLOW:
return getBaseSpeed();
case AFRICAN_SWALLOW:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE_PARROT:
return isNailed() ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException("未知的鳥類");
}
}重構後:
public abstract class Bird {
public abstract double getSpeed();
}
public class EuropeanSwallow extends Bird {
@Override
public double getSpeed() {
return getBaseSpeed();
}
}
public class AfricanSwallow extends Bird {
private int numberOfCoconuts;
@Override
public double getSpeed() {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
public class NorwegianBlueParrot extends Bird {
private boolean nailed;
private double voltage;
@Override
public double getSpeed() {
return nailed ? 0 : getBaseSpeed(voltage);
}
}4.4 重構方法選擇流程
flowchart TD
A[發現程式碼壞味道] --> B{判斷壞味道類型}
B -->|方法過長| C[Extract Method]
B -->|參數過多| D[Introduce Parameter Object]
B -->|重複程式碼| E[Extract Method 或 Pull Up Method]
B -->|類別過大| F[Extract Class]
B -->|條件邏輯複雜| G[Replace Conditional with Polymorphism]
B -->|資料與行為分離| H[Move Method]
C --> I[執行測試]
D --> I
E --> I
F --> I
G --> I
H --> I
I --> J{測試通過?}
J -->|是| K[提交變更]
J -->|否| L[回復變更並修正]
L --> I4.4.1 重構決策樹
當遇到程式碼壞味道時,可以按照以下決策樹選擇合適的重構方法:
flowchart TD
START[開始分析程式碼] --> SCOPE{分析範圍}
SCOPE -->|單一方法內| METHOD_LEVEL[方法層級問題]
SCOPE -->|多個方法/類別| CLASS_LEVEL[類別層級問題]
SCOPE -->|系統架構| SYSTEM_LEVEL[系統層級問題]
METHOD_LEVEL --> METHOD_SIZE{方法大小}
METHOD_SIZE -->|行數 > 30| EXTRACT_METHOD[Extract Method]
METHOD_SIZE -->|參數 > 4| PARAM_OBJECT[Introduce Parameter Object]
METHOD_SIZE -->|巢狀過深| DECOMPOSE[Decompose Conditional]
CLASS_LEVEL --> CLASS_SIZE{類別大小}
CLASS_SIZE -->|行數 > 300| EXTRACT_CLASS[Extract Class]
CLASS_SIZE -->|職責混亂| MOVE_METHOD[Move Method/Field]
CLASS_SIZE -->|繼承問題| INHERITANCE[Pull Up/Push Down Method]
SYSTEM_LEVEL --> ARCH_ISSUE{架構問題}
ARCH_ISSUE -->|層級混亂| LAYER_REFACTOR[分層重構]
ARCH_ISSUE -->|依賴複雜| DI_PATTERN[依賴注入模式]
ARCH_ISSUE -->|性能問題| PERF_REFACTOR[效能重構]
style START fill:#e1f5fe
style EXTRACT_METHOD fill:#c8e6c9
style PARAM_OBJECT fill:#c8e6c9
style EXTRACT_CLASS fill:#c8e6c94.4.2 重構優先順序指南
高優先級(立即處理):
- 安全性問題相關的程式碼壞味道
- 影響系統穩定性的複雜條件邏輯
- 關鍵業務流程中的重複程式碼
中優先級(計劃處理):
- 影響開發效率的過長方法
- 難以測試的緊耦合類別
- 命名不清晰的變數和方法
低優先級(有時間再處理):
- 註解過多但功能正常的程式碼
- 風格不一致但邏輯清晰的程式碼
- 效能可接受的次優化實作
4.5 實務練習
請嘗試重構以下程式碼:
public class OrderService {
public String processOrder(String customerType, double orderAmount,
int loyaltyPoints, boolean hasPromoCode) {
double discount = 0;
double finalAmount = orderAmount;
// 計算折扣
if (customerType.equals("VIP")) {
discount = 0.2;
} else if (customerType.equals("Premium")) {
discount = 0.15;
} else if (customerType.equals("Regular")) {
discount = 0.1;
}
// 忠誠點數額外折扣
if (loyaltyPoints > 1000) {
discount += 0.05;
}
// 促銷代碼額外折扣
if (hasPromoCode) {
discount += 0.1;
}
finalAmount = orderAmount * (1 - discount);
// 發送通知郵件
String message = "您的訂單已處理完成";
if (customerType.equals("VIP")) {
message = "尊敬的 VIP 客戶,您的訂單已優先處理完成";
}
return "訂單處理成功,最終金額:" + finalAmount + ",通知訊息:" + message;
}
}建議的重構步驟:
- 提取折扣計算邏輯
- 使用枚舉取代字串常數
- 引入參數物件
- 提取通知訊息生成邏輯
5. 重構與測試的關聯
5.1 重構的安全網:單元測試
重構的核心在於保持外部行為不變,而單元測試就是確保這一點的安全網。
graph TD
A[程式碼] --> B[單元測試]
B --> C{測試覆蓋率足夠?}
C -->|是| D[安全重構]
C -->|否| E[補充測試]
E --> D
D --> F[執行測試]
F --> G{測試通過?}
G -->|是| H[重構成功]
G -->|否| I[回復變更]
I --> J[分析問題]
J --> D
style B fill:#ccffcc
style D fill:#ffffcc
style H fill:#ccffff5.2 測試先行的重構策略
5.2.1 現有程式碼的測試補強
當面對沒有測試的舊程式碼時:
步驟 1:特性測試(Characterization Tests)
// 原始程式碼(沒有測試)
public class LegacyOrderCalculator {
public double calculateTotal(List<Item> items, String customerType) {
double total = 0;
for (Item item : items) {
total += item.getPrice() * item.getQuantity();
}
// 複雜的折扣邏輯
if (customerType.equals("VIP")) {
total = total * 0.8;
} else if (customerType.equals("Premium")) {
total = total * 0.9;
}
return total;
}
}步驟 2:建立特性測試
@Test
public class LegacyOrderCalculatorTest {
private LegacyOrderCalculator calculator = new LegacyOrderCalculator();
@Test
public void testVipCustomerDiscount() {
// 準備測試資料
List<Item> items = Arrays.asList(
new Item("產品A", 100.0, 2),
new Item("產品B", 50.0, 1)
);
// 執行並驗證現有行為
double result = calculator.calculateTotal(items, "VIP");
assertEquals(200.0, result, 0.01); // (100*2 + 50*1) * 0.8 = 200
}
@Test
public void testPremiumCustomerDiscount() {
List<Item> items = Arrays.asList(
new Item("產品A", 100.0, 1)
);
double result = calculator.calculateTotal(items, "Premium");
assertEquals(90.0, result, 0.01); // 100 * 0.9 = 90
}
@Test
public void testRegularCustomerNoDiscount() {
List<Item> items = Arrays.asList(
new Item("產品A", 100.0, 1)
);
double result = calculator.calculateTotal(items, "Regular");
assertEquals(100.0, result, 0.01); // 100 * 1 = 100
}
}5.2.2 重構過程中的測試策略
重構前:執行完整測試
# Maven 專案
mvn test
# 確保所有測試通過
[INFO] Tests run: 15, Failures: 0, Errors: 0, Skipped: 0重構中:頻繁執行測試
// 重構步驟 1:提取方法
public double calculateTotal(List<Item> items, String customerType) {
double subtotal = calculateSubtotal(items);
double discount = calculateDiscount(customerType);
return subtotal * (1 - discount);
}
private double calculateSubtotal(List<Item> items) {
double total = 0;
for (Item item : items) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
// 立即執行測試
// mvn test -Dtest=LegacyOrderCalculatorTest5.3 TDD 與重構的結合
5.3.1 紅燈-綠燈-重構循環詳解
sequenceDiagram
participant D as 開發者
participant T as 測試
participant C as 程式碼
D->>T: 1. 寫失敗測試(紅燈)
T-->>D: 測試失敗
D->>C: 2. 寫最少程式碼讓測試通過
D->>T: 執行測試
T-->>D: 測試通過(綠燈)
D->>C: 3. 重構改善程式碼品質
D->>T: 執行測試確保功能不變
T-->>D: 測試仍然通過
D->>D: 重複循環5.3.2 實際 TDD 重構範例
需求: 建立一個計算機類別,支援加減乘除運算
第一個循環:加法
// 1. 紅燈:寫失敗測試
@Test
public void testAddition() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
}
// 2. 綠燈:最少程式碼
public class Calculator {
public int add(int a, int b) {
return 5; // 硬編碼讓測試通過
}
}
// 3. 重構:改善實作
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}第二個循環:減法
// 1. 紅燈:新測試
@Test
public void testSubtraction() {
Calculator calc = new Calculator();
assertEquals(1, calc.subtract(3, 2));
}
// 2. 綠燈:添加功能
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
// 3. 重構:沒有明顯的重構需求,繼續下個循環5.4 重構時的測試最佳實務
5.4.1 測試覆蓋率要求
// 使用 JaCoCo 檢查測試覆蓋率
// pom.xml 配置
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>覆蓋率目標:
- 行覆蓋率:至少 80%
- 分支覆蓋率:至少 70%
- 方法覆蓋率:至少 90%
5.4.2 重構友善的測試設計
❌ 脆弱的測試(容易因重構而失敗)
@Test
public void testOrderProcessingInternalDetails() {
Order order = new Order();
order.addItem(new Item("產品A", 100));
// 測試內部實作細節
assertEquals(1, order.getItems().size());
assertEquals("產品A", order.getItems().get(0).getName());
assertEquals(100.0, order.getItems().get(0).getPrice());
}✅ 穩健的測試(專注於行為而非實作)
@Test
public void testOrderTotalCalculation() {
Order order = new Order();
order.addItem(new Item("產品A", 100));
order.addItem(new Item("產品B", 50));
// 測試外部行為
assertEquals(150.0, order.getTotal());
}5.4.3 測試替身在重構中的應用
public class OrderService {
private PaymentGateway paymentGateway;
private EmailService emailService;
public OrderResult processOrder(Order order) {
// 處理付款
PaymentResult payment = paymentGateway.processPayment(order.getTotal());
if (payment.isSuccessful()) {
// 發送確認郵件
emailService.sendConfirmation(order.getCustomerEmail());
return OrderResult.success(order.getId());
} else {
return OrderResult.failure("付款失敗");
}
}
}
@Test
public void testSuccessfulOrderProcessing() {
// 使用 Mock 物件
PaymentGateway mockPayment = mock(PaymentGateway.class);
EmailService mockEmail = mock(EmailService.class);
when(mockPayment.processPayment(100.0))
.thenReturn(PaymentResult.successful());
OrderService service = new OrderService(mockPayment, mockEmail);
Order order = new Order("customer@example.com", 100.0);
OrderResult result = service.processOrder(order);
assertTrue(result.isSuccessful());
verify(mockEmail).sendConfirmation("customer@example.com");
}5.5 重構測試檢查清單
在進行重構前,請確認:
- 現有測試覆蓋率足夠(至少 80%)
- 所有測試都能通過
- 測試專注於行為而非實作細節
- 有適當的測試替身處理外部依賴
- 準備了回歸測試來驗證重構結果
在重構過程中:
- 每次小幅修改後立即執行測試
- 測試失敗時立即停止並修正
- 保持測試的綠燈狀態
- 定期檢查測試覆蓋率
6. 實務應用策略
6.1 重構時機的判斷
6.1.1 主動重構 vs 被動重構
graph LR
A[程式碼狀況] --> B{評估重構需求}
B --> C[主動重構]
B --> D[被動重構]
C --> E[定期程式碼檢視]
C --> F[效能優化需求]
C --> G[架構改善計畫]
D --> H[新功能開發前]
D --> I[Bug 修復時]
D --> J[程式碼審查發現問題]
style C fill:#ccffcc
style D fill:#ffffcc6.1.2 重構優先順序矩陣
| 影響範圍 / 複雜度 | 低複雜度 | 中複雜度 | 高複雜度 |
|---|---|---|---|
| 高影響 | 🟢 立即執行 | 🟡 規劃執行 | 🔴 謹慎評估 |
| 中影響 | 🟢 適時執行 | 🟡 排程執行 | 🟡 分階段執行 |
| 低影響 | 🟢 有空時執行 | ⚪ 可考慮延後 | ⚪ 非必要 |
6.1.3 重構效益評估
成本考量:
- 開發時間投入
- 測試驗證成本
- 程式碼審查時間
- 潛在風險成本
效益評估:
- 維護成本降低
- 開發效率提升
- Bug 減少
- 新功能開發加速
// 範例:重構效益計算
public class RefactoringBenefitCalculator {
public RefactoringDecision evaluateRefactoring(CodeMetrics metrics) {
double complexityScore = calculateComplexityScore(metrics);
double maintainabilityScore = calculateMaintainabilityScore(metrics);
double riskScore = calculateRiskScore(metrics);
double benefitScore = (complexityScore + maintainabilityScore) / 2;
double costScore = riskScore;
if (benefitScore > costScore * 1.5) {
return RefactoringDecision.PROCEED;
} else if (benefitScore > costScore) {
return RefactoringDecision.CONSIDER;
} else {
return RefactoringDecision.POSTPONE;
}
}
}6.2 團隊重構策略
6.2.1 重構責任分配
graph TD
A[程式碼擁有者] --> B[小規模重構]
C[團隊領導] --> D[中規模重構]
E[架構師] --> F[大規模重構]
B --> G[方法級重構<br/>變數重新命名]
D --> H[類別重構<br/>模組重新組織]
F --> I[架構級重構<br/>設計模式引入]
style A fill:#ccffcc
style C fill:#ffffcc
style E fill:#ffcccc6.2.2 協作重構流程
第一階段:準備
- 建立重構分支
- 確保測試覆蓋率
- 建立回滾計畫
第二階段:執行
- 小步重構
- 頻繁提交
- 持續整合
第三階段:整合
- 程式碼審查
- 效能測試
- 合併主分支
6.3 大型專案重構策略
6.3.1 漸進式重構(Strangler Fig Pattern)
graph LR
A[舊系統] --> B[包裝層]
B --> C[新系統]
D[新功能] --> C
E[既有功能] --> B
B --> A
style A fill:#ffcccc
style B fill:#ffffcc
style C fill:#ccffcc實作步驟:
// 步驟 1:建立介面包裝舊系統
public interface OrderService {
OrderResult processOrder(OrderRequest request);
}
public class LegacyOrderServiceWrapper implements OrderService {
private LegacyOrderSystem legacySystem;
@Override
public OrderResult processOrder(OrderRequest request) {
// 轉換請求格式
LegacyOrderRequest legacyRequest = convertToLegacyFormat(request);
// 呼叫舊系統
LegacyOrderResponse legacyResponse = legacySystem.process(legacyRequest);
// 轉換回應格式
return convertToNewFormat(legacyResponse);
}
}
// 步驟 2:逐步替換為新實作
public class NewOrderService implements OrderService {
@Override
public OrderResult processOrder(OrderRequest request) {
// 新的實作邏輯
return processWithNewLogic(request);
}
}
// 步驟 3:使用 Feature Toggle 控制切換
public class OrderServiceFactory {
@Value("${feature.new-order-service.enabled:false}")
private boolean useNewService;
public OrderService createOrderService() {
if (useNewService) {
return new NewOrderService();
} else {
return new LegacyOrderServiceWrapper();
}
}
}6.3.2 分支抽象(Branch by Abstraction)
// 步驟 1:建立抽象層
public abstract class PaymentProcessor {
public abstract PaymentResult process(PaymentRequest request);
}
// 步驟 2:包裝現有實作
public class OldPaymentProcessor extends PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
// 現有的付款處理邏輯
return legacyPaymentSystem.processPayment(request);
}
}
// 步驟 3:建立新實作
public class NewPaymentProcessor extends PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
// 新的付款處理邏輯
return newPaymentGateway.processPayment(request);
}
}
// 步驟 4:逐步切換
public class PaymentService {
private PaymentProcessor processor;
public PaymentService(@Qualifier("paymentProcessor") PaymentProcessor processor) {
this.processor = processor;
}
public PaymentResult processPayment(PaymentRequest request) {
return processor.process(request);
}
}6.4 效能考量
6.4.1 重構對效能的影響監控
@Component
public class PerformanceMonitor {
@Around("@annotation(Monitored)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.nanoTime();
try {
Object result = joinPoint.proceed();
long endTime = System.nanoTime();
logPerformance(joinPoint.getSignature().getName(),
endTime - startTime, "SUCCESS");
return result;
} catch (Exception e) {
long endTime = System.nanoTime();
logPerformance(joinPoint.getSignature().getName(),
endTime - startTime, "ERROR");
throw e;
}
}
private void logPerformance(String methodName, long duration, String status) {
logger.info("Method: {}, Duration: {}ns, Status: {}",
methodName, duration, status);
// 發送監控指標到監控系統
metricsCollector.recordMethodDuration(methodName, duration);
}
}6.4.2 效能基準測試
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class OrderProcessingBenchmark {
private OrderService oldOrderService;
private OrderService newOrderService;
private OrderRequest testRequest;
@Setup
public void setup() {
oldOrderService = new OldOrderService();
newOrderService = new NewOrderService();
testRequest = createTestOrderRequest();
}
@Benchmark
public OrderResult benchmarkOldService() {
return oldOrderService.processOrder(testRequest);
}
@Benchmark
public OrderResult benchmarkNewService() {
return newOrderService.processOrder(testRequest);
}
}6.4.3 重構工具選擇與配置
IDE 內建重構工具比較:
| 功能 | IntelliJ IDEA | Eclipse | VS Code |
|---|---|---|---|
| 自動重新命名 | ✅ 優秀 | ✅ 良好 | ✅ 良好 |
| Extract Method | ✅ 優秀 | ✅ 良好 | ✅ 基本 |
| Move Class/Method | ✅ 優秀 | ✅ 良好 | ❌ 限制 |
| Inline 重構 | ✅ 優秀 | ✅ 良好 | ❌ 無 |
| 安全刪除 | ✅ 優秀 | ✅ 良好 | ❌ 無 |
| 重構預覽 | ✅ 優秀 | ✅ 良好 | ⚠️ 基本 |
自動化重構檢查工具:
<!-- Maven 配置:SonarQube + Checkstyle + PMD -->
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.9.1.2184</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>3.15.0</version>
<configuration>
<targetJdk>11</targetJdk>
<rulesets>
<ruleset>/rulesets/java/quickstart.xml</ruleset>
</rulesets>
</configuration>
</plugin>6.5 重構實務指引
重構前的準備檢查清單:
- 確認業務需求穩定
- 評估重構風險與效益
- 準備充足的測試覆蓋
- 規劃重構範圍與步驟
- 設定效能基準線
- 準備回滾計畫
重構執行要點:
- 遵循小步快跑原則
- 保持測試綠燈狀態
- 頻繁提交程式碼
- 監控系統效能
- 及時溝通進度
重構後的驗證:
- 功能完整性測試
- 效能回歸測試
- 安全性檢查
- 使用者體驗驗證
- 文件更新
7. 團隊規範與最佳實務
7.1 團隊重構約定
7.1.1 重構權限與責任
graph TD
A[重構範圍] --> B[個人層級<br/>1-2天完成]
A --> C[團隊層級<br/>1-2週完成]
A --> D[專案層級<br/>1個月以上]
B --> E[開發者自主決定]
C --> F[需要 Tech Lead 批准]
D --> G[需要架構師與 PM 同意]
E --> H[方法重構<br/>變數重新命名<br/>程式碼清理]
F --> I[類別重構<br/>模組重新組織<br/>設計模式引入]
G --> J[架構重構<br/>技術棧變更<br/>大規模重新設計]
style E fill:#ccffcc
style F fill:#ffffcc
style G fill:#ffcccc7.1.2 重構決策矩陣
| 重構類型 | 複雜度 | 影響範圍 | 決策者 | 審查要求 |
|---|---|---|---|---|
| 方法內重構 | 低 | 單一方法 | 開發者 | 自我審查 |
| 類別重構 | 中 | 單一類別 | 開發者 | Pair Review |
| 模組重構 | 中-高 | 多個類別 | Tech Lead | Team Review |
| 架構重構 | 高 | 整個系統 | 架構師 | Architecture Review |
7.2 程式碼審查中的重構指引
7.2.1 審查檢查清單
功能性檢查:
- 外部行為是否保持不變
- 所有測試是否通過
- 是否新增了必要的測試
- 是否有遺漏的邊界情況
品質檢查:
- 程式碼可讀性是否提升
- 是否遵循 SOLID 原則
- 是否消除了重複程式碼
- 方法和類別的職責是否單一
效能檢查:
- 是否引入效能瓶頸
- 記憶體使用是否合理
- 資料庫查詢是否優化
- 快取策略是否適當
7.2.2 審查意見範例
✅ 建設性的審查意見:
// 建議重構建議
這個方法有點長(45行),建議考慮提取以下邏輯:
1. 資料驗證邏輯 (line 10-20)
2. 業務計算邏輯 (line 25-35)
3. 結果封裝邏輯 (line 40-45)
這樣可以提高可讀性和可測試性。❌ 非建設性的審查意見:
// 不好的審查意見
這個程式碼很亂,需要重寫。7.3 重構標準與約定
7.3.1 命名規範
類別命名:
// ✅ 良好的類別命名
public class OrderCalculator { }
public class UserValidator { }
public class PaymentGateway { }
// ❌ 不佳的類別命名
public class OrderUtils { } // 太籠統
public class DataProcessor { } // 職責不明確
public class Manager { } // 沒有意義方法命名:
// ✅ 良好的方法命名
public void calculateTotalAmount() { }
public boolean isValidEmail(String email) { }
public List<Order> findOrdersByCustomerId(Long customerId) { }
// ❌ 不佳的方法命名
public void calc() { } // 太簡短
public boolean check(String s) { } // 不明確
public void doStuff() { } // 無意義7.3.2 程式碼組織約定
包結構規範:
com.company.project
├── config/ # 配置相關
├── controller/ # 控制器層
├── service/ # 業務服務層
│ ├── impl/ # 服務實作
│ └── dto/ # 資料傳輸物件
├── repository/ # 資料存取層
├── domain/ # 領域模型
│ ├── entity/ # 實體類別
│ ├── valueobject/ # 值物件
│ └── aggregate/ # 聚合根
└── util/ # 工具類別類別職責劃分:
// ✅ 單一職責的服務類別
@Service
public class OrderService {
// 只負責訂單相關的業務邏輯
public OrderResult processOrder(OrderRequest request) { }
public void cancelOrder(Long orderId) { }
public OrderStatus getOrderStatus(Long orderId) { }
}
@Service
public class PaymentService {
// 只負責付款相關的業務邏輯
public PaymentResult processPayment(PaymentRequest request) { }
public void refundPayment(Long paymentId) { }
}
// ❌ 違反單一職責的類別
@Service
public class OrderManagerService {
// 混合了多種職責
public OrderResult processOrder(OrderRequest request) { }
public PaymentResult processPayment(PaymentRequest request) { }
public void sendEmail(String to, String content) { }
public void generateReport() { }
}7.4 持續重構的團隊文化
7.4.1 重構意識培養
定期技術分享:
graph LR
A[每週技術分享] --> B[重構案例分享]
A --> C[設計模式學習]
A --> D[程式碼審查技巧]
B --> E[實際重構前後對比]
C --> F[模式應用場景]
D --> G[常見問題與解決方案]
style A fill:#ccffcc重構激勵機制:
- 重構貢獻獎勵:記錄團隊成員的重構貢獻
- 程式碼品質指標:定期檢視程式碼品質趨勢
- 最佳實務分享:鼓勵分享重構經驗
7.4.2 工具與流程整合
CI/CD 整合重構檢查:
# .github/workflows/code-quality.yml
name: Code Quality Check
on:
pull_request:
branches: [ main, develop ]
jobs:
code-quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Cache Maven packages
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
- name: Run tests
run: mvn test
- name: Run SonarQube analysis
run: mvn sonar:sonar -Dsonar.projectKey=my-project
- name: Check test coverage
run: mvn jacoco:check
- name: Checkstyle check
run: mvn checkstyle:checkIDE 設定同步:
// .vscode/settings.json
{
"java.format.settings.url": "./eclipse-formatter.xml",
"java.checkstyle.configuration": "./checkstyle.xml",
"java.test.defaultConfig": "default",
"sonarlint.connectedMode.project": {
"connectionId": "my-sonarqube",
"projectKey": "my-project"
}
}7.5 重構培訓計畫
7.5.1 新人重構訓練
第一週:基礎概念
- 重構定義與目標
- 程式碼壞味道識別
- 基本重構技巧
- 工具使用
第二週:實作練習
- 結對重構練習
- 程式碼審查參與
- 測試驅動重構
- 專案實戰
第三週:進階技巧
- 設計模式應用
- 架構重構策略
- 效能考量
- 風險控制
7.5.2 技能評估標準
初級重構師(Junior)
- 能識別基本的程式碼壞味道
- 能執行簡單的方法層級重構
- 理解重構與測試的關係
- 能使用 IDE 重構工具
中級重構師(Intermediate)
- 能設計類別層級的重構策略
- 能在重構中保持測試綠燈
- 理解設計模式在重構中的應用
- 能評估重構的風險與效益
高級重構師(Senior)
- 能領導大規模重構專案
- 能設計漸進式重構策略
- 能處理遺留系統重構
- 能建立團隊重構規範
7.6 團隊重構最佳實務
建立重構文化:
- 定期舉辦重構分享會
- 建立重構案例庫
- 設立程式碼品質指標
- 鼓勵重構實驗與創新
工具與流程:
- 統一開發環境設定
- 整合靜態分析工具
- 建立自動化品質檢查
- 提供重構指引文件
技能發展:
- 制定重構培訓計畫
- 建立技能評估標準
- 提供重構實作機會
- 定期技術能力評估
8. 考試重點與練習
8.1 認證考試重點
8.1.1 理論知識考點
重構基本概念(20%)
- 重構的定義與目標
- 重構 vs 重寫的差異
- 重構的時機選擇
- 重構的風險與效益
程式碼壞味道識別(25%)
- 常見壞味道的特徵
- 壞味道的成因分析
- 壞味道的影響評估
- 壞味道的優先處理順序
重構技巧應用(30%)
- 方法層級重構技巧
- 類別層級重構技巧
- 條件邏輯重構
- 繼承關係重構
測試與重構(15%)
- TDD 與重構的關係
- 重構中的測試策略
- 測試覆蓋率要求
- 回歸測試設計
團隊協作(10%)
- 重構決策流程
- 程式碼審查要點
- 持續整合實務
- 風險控制策略
8.1.2 實作技能考點
程式碼分析能力
// 考試題型:識別以下程式碼的壞味道並提出重構建議
public class CustomerManager {
public String processCustomer(String name, String email, String phone,
String address, String type, boolean active) {
// 驗證邏輯
if (name == null || name.trim().isEmpty()) return "姓名不能為空";
if (email == null || !email.contains("@")) return "電子郵件格式錯誤";
if (phone == null || phone.length() < 10) return "電話號碼格式錯誤";
// 處理邏輯
if (type.equals("VIP")) {
// VIP 客戶處理
saveToDatabase(name, email, phone, address, type, active);
sendWelcomeEmail(email, "VIP");
createLoyaltyAccount(email);
return "VIP 客戶建立成功";
} else if (type.equals("Premium")) {
// Premium 客戶處理
saveToDatabase(name, email, phone, address, type, active);
sendWelcomeEmail(email, "Premium");
return "Premium 客戶建立成功";
} else {
// 一般客戶處理
saveToDatabase(name, email, phone, address, type, active);
sendWelcomeEmail(email, "Regular");
return "一般客戶建立成功";
}
}
}重構實作能力
考試要求學員能夠:
- 識別上述程式碼的問題
- 提出具體的重構步驟
- 實作重構後的程式碼
- 撰寫相應的單元測試
8.2 練習題庫
8.2.1 選擇題練習
題目 1: 以下哪個不是程式碼壞味道? A) Long Method(過長方法) B) Duplicate Code(重複程式碼) C) Single Responsibility(單一職責) D) Large Class(過大類別)
答案:C
題目 2: 在 TDD 循環中,重構應該在什麼時候進行? A) 寫測試之前 B) 測試失敗時 C) 測試通過後 D) 專案結束時
答案:C
題目 3: Extract Method 重構技巧主要用於解決什麼問題? A) 過多參數 B) 過長方法 C) 重複程式碼 D) 過大類別
答案:B
8.2.2 情境分析題
題目: 你正在進行一個關鍵業務系統的重構,以下哪種做法最適合?
// 現有程式碼
public class OrderProcessor {
public void processOrder(Order order) {
// 150 行程式碼包含:
// - 訂單驗證
// - 庫存檢查
// - 價格計算
// - 付款處理
// - 發送通知
// - 記錄日誌
}
}A) 一次性重寫整個方法 B) 逐步提取小方法,每次重構後執行測試 C) 先刪除現有程式碼,再重新開發 D) 保持現狀,不進行重構
標準答案:B
解釋: 關鍵業務系統應該採用風險最低的漸進式重構方法。逐步提取小方法可以:
- 降低重構風險
- 保持系統穩定
- 便於測試驗證
- 易於回滾變更
8.2.3 程式碼重構實作題
題目:重構以下計算機類別
public class Calculator {
public double calculate(double a, double b, String operation) {
if (operation.equals("add")) {
return a + b;
} else if (operation.equals("subtract")) {
return a - b;
} else if (operation.equals("multiply")) {
return a * b;
} else if (operation.equals("divide")) {
if (b == 0) {
throw new IllegalArgumentException("不能除以零");
}
return a / b;
} else {
throw new IllegalArgumentException("不支援的運算:" + operation);
}
}
}參考答案:
// 步驟 1:引入列舉
public enum Operation {
ADD, SUBTRACT, MULTIPLY, DIVIDE
}
// 步驟 2:重構主要方法
public class Calculator {
public double calculate(double a, double b, Operation operation) {
switch (operation) {
case ADD:
return add(a, b);
case SUBTRACT:
return subtract(a, b);
case MULTIPLY:
return multiply(a, b);
case DIVIDE:
return divide(a, b);
default:
throw new IllegalArgumentException("不支援的運算:" + operation);
}
}
private double add(double a, double b) {
return a + b;
}
private double subtract(double a, double b) {
return a - b;
}
private double multiply(double a, double b) {
return a * b;
}
private double divide(double a, double b) {
if (b == 0) {
throw new IllegalArgumentException("不能除以零");
}
return a / b;
}
}
// 步驟 3:更進一步使用策略模式
public interface CalculationStrategy {
double calculate(double a, double b);
}
public class AdditionStrategy implements CalculationStrategy {
@Override
public double calculate(double a, double b) {
return a + b;
}
}
public class Calculator {
private Map<Operation, CalculationStrategy> strategies = Map.of(
Operation.ADD, new AdditionStrategy(),
Operation.SUBTRACT, (a, b) -> a - b,
Operation.MULTIPLY, (a, b) -> a * b,
Operation.DIVIDE, (a, b) -> {
if (b == 0) throw new IllegalArgumentException("不能除以零");
return a / b;
}
);
public double calculate(double a, double b, Operation operation) {
CalculationStrategy strategy = strategies.get(operation);
if (strategy == null) {
throw new IllegalArgumentException("不支援的運算:" + operation);
}
return strategy.calculate(a, b);
}
}8.3 模擬考試
8.3.1 考試時間分配
總時間:120 分鐘
- 選擇題(30 題):30 分鐘
- 程式碼分析題(5 題):40 分鐘
- 重構實作題(3 題):50 分鐘
8.3.2 評分標準
選擇題(30%)
- 每題 1 分,共 30 分
- 答對率需達 80% 以上
程式碼分析題(35%)
- 壞味道識別:5 分/題
- 重構建議:5 分/題
- 風險評估:5 分/題
重構實作題(35%)
- 程式碼正確性:10 分/題
- 重構技巧運用:5 分/題
- 測試完整性:5 分/題
8.3.3 完整模擬考試
模擬考試 A 卷
第一部分:選擇題(30分)
以下哪個是重構的主要目標? A) 增加新功能 B) 修復錯誤 C) 改善程式碼結構但保持外部行為不變 D) 提升程式效能
什麼是「Feature Envy」程式碼壞味道? A) 方法過長 B) 方法對其他類別的資料比對自己的資料更感興趣 C) 類別過大 D) 重複程式碼
在 TDD 循環中,重構應該在什麼時候進行? A) 寫測試之前 B) 測試失敗時 C) 測試通過後 D) 專案結束時
第二部分:程式碼分析題(40分)
題目 1:識別程式碼壞味道(20分)
public class ReportGenerator {
public String generateReport(String customerType, List<Order> orders,
String format, boolean includeDetails,
Date startDate, Date endDate,
String sortBy, boolean ascending) {
StringBuilder report = new StringBuilder();
// 過濾訂單
List<Order> filteredOrders = new ArrayList<>();
for (Order order : orders) {
if (order.getDate().after(startDate) && order.getDate().before(endDate)) {
if (customerType.equals("VIP") && order.getCustomer().getType().equals("VIP")) {
filteredOrders.add(order);
} else if (customerType.equals("Regular") && !order.getCustomer().getType().equals("VIP")) {
filteredOrders.add(order);
} else if (customerType.equals("All")) {
filteredOrders.add(order);
}
}
}
// 排序
if (sortBy.equals("date")) {
filteredOrders.sort((o1, o2) -> ascending ?
o1.getDate().compareTo(o2.getDate()) :
o2.getDate().compareTo(o1.getDate()));
} else if (sortBy.equals("amount")) {
filteredOrders.sort((o1, o2) -> ascending ?
Double.compare(o1.getAmount(), o2.getAmount()) :
Double.compare(o2.getAmount(), o1.getAmount()));
}
// 生成報表
if (format.equals("CSV")) {
report.append("Order ID,Customer,Amount,Date\n");
for (Order order : filteredOrders) {
report.append(order.getId()).append(",")
.append(order.getCustomer().getName()).append(",")
.append(order.getAmount()).append(",")
.append(order.getDate()).append("\n");
if (includeDetails) {
for (OrderItem item : order.getItems()) {
report.append(" ").append(item.getName()).append(",")
.append(item.getQuantity()).append(",")
.append(item.getPrice()).append("\n");
}
}
}
} else if (format.equals("JSON")) {
// JSON 格式生成邏輯(50行)
// ...
} else if (format.equals("XML")) {
// XML 格式生成邏輯(60行)
// ...
}
return report.toString();
}
}請識別上述程式碼中的壞味道並說明原因(每個5分,共20分):
第三部分:重構實作題(30分)
題目:重構計算機類別(30分)
將以下程式碼重構為更好的設計:
public class AdvancedCalculator {
public double compute(String expression) {
String[] parts = expression.split(" ");
if (parts.length != 3) {
throw new IllegalArgumentException("Invalid expression");
}
double num1 = Double.parseDouble(parts[0]);
String operator = parts[1];
double num2 = Double.parseDouble(parts[2]);
if (operator.equals("+")) {
return num1 + num2;
} else if (operator.equals("-")) {
return num1 - num2;
} else if (operator.equals("*")) {
return num1 * num2;
} else if (operator.equals("/")) {
if (num2 == 0) {
throw new IllegalArgumentException("Division by zero");
}
return num1 / num2;
} else if (operator.equals("^")) {
return Math.pow(num1, num2);
} else if (operator.equals("%")) {
return num1 % num2;
} else {
throw new IllegalArgumentException("Unsupported operator: " + operator);
}
}
}要求:
- 應用適當的重構技巧(15分)
- 提供完整的單元測試(10分)
- 說明重構的理由和好處(5分)
參考答案:
選擇題答案:
- C 2. B 3. C
程式碼分析題答案:
- Long Parameter List(過多參數) - 方法有8個參數,建議使用 Parameter Object
- Long Method(過長方法) - 方法承擔多種職責:過濾、排序、格式化
- Switch Statement / Conditional Complexity(複雜條件邏輯) - 多個 if-else 判斷,建議使用 Strategy Pattern
- Duplicate Code(重複程式碼) - 排序邏輯和格式化邏輯有重複結構
重構實作題參考答案:
// 1. 引入策略模式
public enum Operator {
ADD("+", (a, b) -> a + b),
SUBTRACT("-", (a, b) -> a - b),
MULTIPLY("*", (a, b) -> a * b),
DIVIDE("/", (a, b) -> {
if (b == 0) throw new IllegalArgumentException("Division by zero");
return a / b;
}),
POWER("^", Math::pow),
MODULO("%", (a, b) -> a % b);
private final String symbol;
private final BinaryOperator<Double> operation;
Operator(String symbol, BinaryOperator<Double> operation) {
this.symbol = symbol;
this.operation = operation;
}
public static Operator fromSymbol(String symbol) {
return Arrays.stream(values())
.filter(op -> op.symbol.equals(symbol))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unsupported operator: " + symbol));
}
public double apply(double a, double b) {
return operation.apply(a, b);
}
}
// 2. 重構後的計算機
public class AdvancedCalculator {
public double compute(String expression) {
Expression expr = parseExpression(expression);
return expr.evaluate();
}
private Expression parseExpression(String expression) {
String[] parts = expression.trim().split("\\s+");
if (parts.length != 3) {
throw new IllegalArgumentException("Invalid expression format. Expected: 'number operator number'");
}
try {
double operand1 = Double.parseDouble(parts[0]);
Operator operator = Operator.fromSymbol(parts[1]);
double operand2 = Double.parseDouble(parts[2]);
return new Expression(operand1, operator, operand2);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid number format in expression", e);
}
}
}
// 3. 表達式值物件
public class Expression {
private final double operand1;
private final Operator operator;
private final double operand2;
public Expression(double operand1, Operator operator, double operand2) {
this.operand1 = operand1;
this.operator = operator;
this.operand2 = operand2;
}
public double evaluate() {
return operator.apply(operand1, operand2);
}
}
// 4. 測試類別
@Test
public class AdvancedCalculatorTest {
private AdvancedCalculator calculator = new AdvancedCalculator();
@Test
public void testBasicOperations() {
assertEquals(5.0, calculator.compute("2 + 3"), 0.001);
assertEquals(1.0, calculator.compute("3 - 2"), 0.001);
assertEquals(6.0, calculator.compute("2 * 3"), 0.001);
assertEquals(2.0, calculator.compute("6 / 3"), 0.001);
}
@Test
public void testAdvancedOperations() {
assertEquals(8.0, calculator.compute("2 ^ 3"), 0.001);
assertEquals(1.0, calculator.compute("5 % 2"), 0.001);
}
@Test
public void testDivisionByZero() {
assertThrows(IllegalArgumentException.class,
() -> calculator.compute("5 / 0"));
}
@Test
public void testInvalidExpression() {
assertThrows(IllegalArgumentException.class,
() -> calculator.compute("2 +"));
assertThrows(IllegalArgumentException.class,
() -> calculator.compute("abc + 3"));
}
}重構理由:
- 消除條件複雜性:使用枚舉和策略模式取代 if-else 鏈
- 提高擴展性:新增運算子只需要在枚舉中添加
- 職責分離:表達式解析和計算分開處理
- 提高可讀性:程式碼結構更清晰,易於理解和維護
8.4 考試準備建議
理論知識準備:
- 熟記各種程式碼壞味道的特徵
- 理解各種重構技巧的適用場景
- 掌握重構與測試的關係
- 了解團隊重構的流程與規範
實作技能準備:
- 練習使用 IDE 重構工具
- 熟練掌握基本重構技巧
- 能夠設計測試來保護重構
- 具備程式碼分析與評估能力
考試技巧:
- 先做簡單題目確保基本分數
- 重構實作題要先分析再動手
- 預留時間檢查程式碼正確性
- 注意時間分配,避免單題耗時過長
9. 檢查清單
9.1 重構前檢查清單
9.1.1 環境準備
版本控制:
- 確認目前分支狀態乾淨
- 所有變更已提交
- 建立重構專用分支
- 設定適當的分支保護規則
測試準備:
- 執行完整測試套件確認通過
- 檢查測試覆蓋率是否足夠(≥80%)
- 識別高風險區域並補強測試
- 準備效能基準測試
工具設定:
- IDE 重構工具已正確配置
- 靜態分析工具已啟用
- 程式碼格式化設定已同步
- 版本控制 hooks 已設定
9.1.2 範圍評估
重構規模:
- 明確定義重構範圍
- 評估重構複雜度
- 估算所需時間
- 識別相關的系統元件
風險評估:
- 識別潛在的重構風險
- 準備風險應對策略
- 建立回滾計畫
- 設定品質閾值
利害關係人溝通:
- 通知相關開發者
- 確認業務影響
- 獲得必要的批准
- 設定溝通機制
9.2 重構執行檢查清單
9.2.1 執行原則
小步快跑:
- 每次變更範圍控制在最小
- 確保每步都能通過測試
- 頻繁提交程式碼(至少每小時一次)
- 保持功能完全相同
測試驗證:
- 每次修改後立即執行相關測試
- 定期執行完整測試套件
- 監控測試執行時間
- 確保測試覆蓋率不降低
9.2.2 常見重構步驟
Extract Method(提取方法):
- 識別可提取的程式碼片段
- 檢查區域變數和參數
- 建立新方法簽名
- 移動程式碼到新方法
- 在原位置呼叫新方法
- 執行測試驗證
Rename(重新命名):
- 使用 IDE 自動重新命名功能
- 檢查所有參考位置
- 更新相關文件和註解
- 驗證命名約定一致性
- 執行測試確認無誤
Move Method(搬移方法):
- 分析方法依賴關係
- 確認目標類別適合性
- 檢查訪問權限
- 更新方法呼叫
- 驗證封裝性
- 執行完整測試
9.3 重構後檢查清單
9.3.1 功能驗證
基本功能:
- 所有單元測試通過
- 整合測試正常
- 端到端測試成功
- 回歸測試無異常
效能驗證:
- 執行效能基準測試
- 比較重構前後效能差異
- 監控記憶體使用情況
- 檢查資源消耗
9.3.2 品質檢查
程式碼品質:
- 靜態分析工具檢查通過
- 程式碼複雜度有所改善
- 壞味道已被消除
- 遵循團隊編碼規範
可維護性:
- 程式碼可讀性提升
- 方法和類別職責更單一
- 重複程式碼已移除
- 註解和文件已更新
9.3.3 交付準備
文件更新:
- API 文件已更新
- 架構圖已修正
- 重構日誌已記錄
- 變更影響已說明
團隊溝通:
- 重構結果已分享
- 經驗學習已整理
- 最佳實務已提取
- 後續改善已規劃
9.4 緊急應對檢查清單
9.4.1 問題發現
立即行動:
- 停止進一步的重構
- 記錄問題現象
- 回復到最後一個穩定版本
- 通知相關團隊成員
問題分析:
- 識別問題根本原因
- 檢查測試覆蓋是否充足
- 分析重構步驟是否過大
- 評估風險控制是否到位
9.4.2 修復策略
短期修復:
- 修復緊急功能問題
- 部署熱修復版本
- 監控系統穩定性
- 更新問題追蹤
長期改善:
- 補強測試覆蓋
- 改善重構流程
- 加強風險控制
- 更新團隊規範
9.5 持續改善檢查清單
9.5.1 定期檢視
週度檢視:
- 檢查程式碼品質趨勢
- 分析重構效果
- 收集團隊回饋
- 調整重構策略
月度檢視:
- 評估重構目標達成度
- 更新重構計畫
- 分享最佳實務
- 培訓新進成員
9.5.2 工具和流程優化
工具改善:
- 評估新的重構工具
- 優化現有工具設定
- 整合品質檢查流程
- 自動化重複性工作
流程改善:
- 簡化重構審查流程
- 改善團隊協作機制
- 優化風險控制措施
- 增強監控能力
10. 參考資源
10.1 延伸閱讀
書籍推薦:
- 《Refactoring: Improving the Design of Existing Code》- Martin Fowler
- 《Clean Code》- Robert C. Martin
- 《Working Effectively with Legacy Code》- Michael Feathers
線上資源:
- Refactoring.guru - 重構技巧和設計模式
- SonarQube 官方文件 - 程式碼品質分析
- JUnit 5 官方文件 - 單元測試指南
10.2 工具推薦
IDE 重構工具:
- IntelliJ IDEA - 最強大的 Java 重構支援
- Eclipse - 豐富的重構插件生態
- VS Code - 輕量級但功能完整的重構工具
靜態分析工具:
- SonarQube - 企業級程式碼品質平台
- Checkstyle - Java 程式碼風格檢查
- PMD - 程式碼缺陷檢測
本教學手冊將隨著團隊經驗累積和工具發展持續更新。如有問題或建議,請隨時聯繫開發團隊。
版本資訊:
- 文件版本:v1.0
- 最後更新:2025年9月1日
- 作者:資深軟體架構師團隊
- 適用對象:Java 開發人員、系統架構師