在等待馬丁大叔的《重構(gòu)》第二版的艱難日子里,恰巧在一本書里看到了一個 C# 的重構(gòu)的例子桩撮,覺得不錯敦第,就轉(zhuǎn)成了 Java 版的,在此記錄一下整個過程店量。
初始版本
這是一個用于計算不同帳戶類型的積分計算的類:
package com.songofcode.refactor.account;
public class Account {
private int balance;
private int rewardPoints;
private AccountType type;
public int getRewardPoints(){
return rewardPoints;
}
public enum AccountType {
Silver,
Gold,
Platinum
}
public Account(AccountType type) {
this.type = type;
}
public void AddTransaction(int amount) {
rewardPoints += calculateRewardPoints(amount);
balance += amount;
}
private int calculateRewardPoints(int amount) {
int points = 0;
switch (type) {
case Silver:
points = amount / 10;
break;
case Gold:
points = (balance / 10000 * 5) + (amount / 5);
break;
case Platinum:
points = (balance / 10000 * 40) + (amount / 2);
break;
default:
points = 0;
break;
}
return points;
}
}
可以看到芜果,這個 Account 類的構(gòu)造函數(shù)中接收一個 AccountType, 在計算積分的時候,根據(jù)這個 type, 會有不同的算法融师,獲取的積分也就不同了右钾。
那么我們來看看這個類可以怎么重構(gòu)呢?(重構(gòu)之前應(yīng)該是要在有單元測試的基礎(chǔ)上的旱爆,這里略去單元測試的代碼)舀射。
去掉 magic numbers
package com.songofcode.refactor.account;
public class Account {
public static final int SILVER_TRANSACTION_COST_PER_POINT = 10;
public static final int GOLD_TRANSACTION_COST_PER_POINT = 5;
public static final int PLATINUM_TRANSACTION_COST_PER_POINT = 40;
public static final int GOLD_BALANCE_COST_PER_POINT = 20000;
public static final int PLATINUM_BALANCE_COST_PER_POINT = 10000;
private int balance;
private int rewardPoints;
private AccountType type;
public Account(AccountType type) {
this.type = type;
}
public int getRewardPoints(){
return rewardPoints;
}
public void AddTransaction(int amount) {
rewardPoints += calculateRewardPoints(amount);
balance += amount;
}
private int calculateRewardPoints(int amount) {
int points = 0;
switch (type) {
case Silver:
points = amount / SILVER_TRANSACTION_COST_PER_POINT;
break;
case Gold:
points = (balance / GOLD_BALANCE_COST_PER_POINT * GOLD_TRANSACTION_COST_PER_POINT) + (amount / GOLD_TRANSACTION_COST_PER_POINT);
break;
case Platinum:
points = (balance / PLATINUM_BALANCE_COST_PER_POINT * PLATINUM_TRANSACTION_COST_PER_POINT) + (amount / PLATINUM_TRANSACTION_COST_PER_POINT);
break;
default:
points = 0;
break;
}
return points;
}
}
這樣做相當(dāng)于給了這些 magic numbers 命名,增強了代碼的可讀性疼鸟。
用多態(tài)替代條件語句
這里的條件語句就是那個 switch 了后控,目前的積分計算邏輯都是在那一大塊 switch 中的代碼里,這樣隨著 AccountType 的種類變多空镜, switch 中的代碼有會越來越多浩淘。 我們可以通過創(chuàng)建 SilverAccount, GoldAccount, PlatinumAccount 來替代 AccoutType. 這樣一來,當(dāng)新的 AccountType 出現(xiàn)時吴攒,只需要新建一個類张抄, 而不需要在 CalculateRewardPoints 方法里增加一個 case 條件,這樣更符合開閉原則洼怔。
創(chuàng)建不同類型的 Account 的 class, 它們都集成了 Account class (要把 Account 改為 Abstract class):
package com.songofcode.refactor.account;
public abstract class Account {
protected int balance;
private int rewardPoints;
private AccountType type;
public int getRewardPoints() {
return rewardPoints;
}
public void AddTransaction(int amount) {
rewardPoints += calculateRewardPoints(amount);
balance += amount;
}
protected abstract int calculateRewardPoints(int amount);
}
可以看到署惯,最復(fù)雜的 calculateRewardPoints 方法變成了抽象方法,同時構(gòu)造函數(shù)也消失了镣隶。 下面就是不同帳戶子類的實現(xiàn)(之前的常量也被分散到了各自的子類中了)极谊。
package com.songofcode.refactor.account;
public class GoldAccount extends Account {
public static final int GOLD_TRANSACTION_COST_PER_POINT = 5;
public static final int GOLD_BALANCE_COST_PER_POINT = 20000;
@Override
public int calculateRewardPoints(int amount) {
return (balance / GOLD_BALANCE_COST_PER_POINT * GOLD_TRANSACTION_COST_PER_POINT) + (amount / GOLD_TRANSACTION_COST_PER_POINT);
}
}
public class SilverAccount extends Account {
public static final int SILVER_TRANSACTION_COST_PER_POINT = 10;
@Override
public int calculateRewardPoints(int amount) {
return amount / SILVER_TRANSACTION_COST_PER_POINT;
}
}
public class PlatinumAccount extends Account {
public static final int PLATINUM_TRANSACTION_COST_PER_POINT = 40;
public static final int PLATINUM_BALANCE_COST_PER_POINT = 10000;
@Override
protected int calculateRewardPoints(int amount) {
return (balance / PLATINUM_BALANCE_COST_PER_POINT * PLATINUM_TRANSACTION_COST_PER_POINT) + (amount / PLATINUM_TRANSACTION_COST_PER_POINT);
}
}
想象一下诡右,這樣做之后,如果要新添加一個新的 Account 類別轻猖,只需要新建一個類帆吻,然后實現(xiàn) calculateRewardPoints 方法就可以了。
用工廠方法替代構(gòu)造函數(shù)
剛才我們把 Account 類改成 Abstract 之后咙边,測試代碼肯定 broken 了猜煮,因為我們沒有一個統(tǒng)一的接口來創(chuàng)建各類 Account 了。 之前我們用 Account 的構(gòu)造函數(shù)來區(qū)分不同的帳戶類別败许,現(xiàn)在可以使用工廠方法來替代它王带。
public abstract class Account {
protected int balance;
private int rewardPoints;
private AccountType type;
public static Account CreateAccount(AccountType type) {
Account account = null;
switch (type) {
case Silver:
account = new SilverAccount();
break;
case Gold:
account = new GoldAccount();
break;
case Platinum:
account = new PlatinumAccount();
break;
}
return account;
}
public int getRewardPoints() {
return rewardPoints;
}
public void AddTransaction(int amount) {
rewardPoints += calculateRewardPoints(amount);
balance += amount;
}
protected abstract int calculateRewardPoints(int amount);
}
這次的改動很小,相較于之前的代碼市殷,只是把構(gòu)造函數(shù)換成了一個靜態(tài)方法愕撰,不過這是過渡的方式,接下來我們要把工廠方法抽取出來醋寝。
一個新的帳戶類型
經(jīng)過之前的重構(gòu)盟戏,讓我們看看當(dāng)新增一個帳戶類型時,需要做哪些改動甥桂。 假設(shè)我們要新增一個青銅級別(bronze)的帳戶。 首先邮旷,需要創(chuàng)建一個 BronzeAccount 的 Account 子類黄选。
public class BronzeAccount extends Account {
public static final int BRONZE_TRANSACTION_COST_PER_POINT = 20;
@Override
public int calculateRewardPoints(int amount) {
return amount / BRONZE_TRANSACTION_COST_PER_POINT;
}
}
這個類中我們定義了青銅帳戶的積分算法,接下來就是要在工廠方法中新增對青銅帳號的支持婶肩。
public static Account CreateAccount(AccountType type) {
Account account = null;
switch (type) {
case Bronze:
account = new BronzeAccount();
break;
case Silver:
account = new SilverAccount();
break;
case Gold:
account = new GoldAccount();
break;
case Platinum:
account = new PlatinumAccount();
break;
}
return account;
}
這樣做的話办陷,每次有新的 accountType 加入,都要修改這個 switch 代碼塊律歼。 我們可以考慮使用元編程來動態(tài)創(chuàng)造 account 實例:
public static Account CreateAccount(String accountType) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class c= Class.forName(accountType + "Account");
return (Account) c.newInstance();
}
不過這種方式太脆弱了民镜,它必須滿足下面幾個條件:
- Account 的類型名必須遵守規(guī)范 [Type]Account
- Account Type 必須和工廠方法在同一個 assembly 中
- 每種 Account Type 必須有一個無參的構(gòu)造方法
如果有這么多限制的話,那通常說明你的重構(gòu)有點過了险毁。
代碼壞味道:拒絕遺贈
假設(shè)我們發(fā)現(xiàn)制圈,不是所有的帳戶都能夠獲取積分的,大部份的帳戶都是普通帳戶畔况,沒有積分方面的需求鲸鹦。 那么我們可以創(chuàng)建一個 StandardAccount 的 Account 子類:
public class StandardAccount extends Account {
protected int calculateRewardPoints(int amount) {
return 0;
}
}
在 StandardAccount 中,把 calculateRewardPoints 這個方法直接返回 0, 這是實現(xiàn)的一種方式跷跪。 在這個例子中馋嗜,父類的抽象方法 calculateRewardPoints 對于子類 StandardAccount 來說,是沒有意義的吵瞻, 甚至是一種累贅葛菇,因此這種現(xiàn)象可以被稱為“拒絕遺贈(refused bequest)”甘磨。
使用代理替代繼承
繼承是一種強耦合關(guān)系,從目前的需求來看眯停,標(biāo)準(zhǔn)帳戶和其他帳戶是不同的兩個種類了济舆。 因此我們需要把積分相關(guān)的邏輯分離出來,比如創(chuàng)建一個接口 IRewardCard
通過讓帳戶持有不同的卡片庵朝,達到不同的積分記錄效果吗冤。這里的“持有”就是把 IReardCard 作為 Account 的構(gòu)造函數(shù)參數(shù)。
public interface IRewardCard {
int getRewardPoints();
void calculateRewardPoints(int amount, int blance);
}
上面是積分卡的接口九府,下面是 Account 類椎瘟,它又變回了一個普通類:
public class Account {
private IRewardCard rewardCard;
private int balance;
public int getBalance() {
return balance;
}
public Account(IRewardCard rewardCard) {
this.rewardCard = rewardCard;
}
public void addTransaction(int amount) {
rewardCard.calculateRewardPoints(amount, balance);
balance += amount;
}
}
只不過構(gòu)造函數(shù)會接收一個 IRewardCard 的實現(xiàn)。 那么我們就以黃金會員卡為例實現(xiàn) IReardCard 接口侄旬。
public class GoldRewardCard implements IRewardCard {
private static final int GOLD_BALANCE_COST_PER_POINT = 20000;
private static final int GOLD_TRANSACTION_COST_PER_POINT = 5;
private int points;
@Override
public int getRewardPoints() {
return points;
}
@Override
public void calculateRewardPoints(int amount, int balance) {
points += (balance / GOLD_BALANCE_COST_PER_POINT * GOLD_TRANSACTION_COST_PER_POINT) + (amount / GOLD_TRANSACTION_COST_PER_POINT);
}
}
相比之前的版本肺蔚,積分的計算邏輯都放到了 RewardCard 中,然后注入到 Account 中儡羔,再由 Account 去調(diào)用 RewardCard 的方法實現(xiàn)積分計算宣羊。
public class AccountTest {
@Test
public void testGoldRewardCard() {
IRewardCard goldRewardCard = new GoldRewardCard();
Account goldAccount = new Account(goldRewardCard);
goldAccount.addTransaction(10000000);
assertEquals(10000000, goldAccount.getBalance());
assertEquals(2000000, goldRewardCard.getRewardPoints());
}
}
現(xiàn)在回到之前的問題:如何處理 StardardAccount ? 這里我們可以使用 Null Object Pattern 來處理。
public class NullRewardCard implements IRewardCard {
@Override
public int getRewardPoints() {
return 0;
}
@Override
public void calculateRewardPoints(int amount, int blance) {}
}
通過向 Account 注入一個 NullRewardCard 來實現(xiàn) standardAccount:
@Test
public void testNullRewardCard() {
IRewardCard nullRewardCard = new NullRewardCard();
Account standardAccount = new Account(nullRewardCard);
standardAccount.addTransaction(10000000);
assertEquals(10000000, standardAccount.getBalance());
assertEquals(0, nullRewardCard.getRewardPoints());
}
可能有人會覺得這兩種實現(xiàn)方式?jīng)]什么區(qū)別汰蜘,現(xiàn)在是 NullRewardCard 返回 0, 之前是 StardardAccount 返回 0. 但我覺得最重要的是仇冯,這樣做分離了 balance 和 points, 這樣 Account 可以專注于處理 balance 相關(guān)的操作, 而 rewardCard 則用于處理 points, 更符合單一職責(zé)原則族操。