一次簡單的重構(gòu)練習(xí)記錄

在等待馬丁大叔的《重構(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();
}

不過這種方式太脆弱了民镜,它必須滿足下面幾個條件:

  1. Account 的類型名必須遵守規(guī)范 [Type]Account
  2. Account Type 必須和工廠方法在同一個 assembly 中
  3. 每種 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é)原則族操。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末苛坚,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子色难,更是在濱河造成了極大的恐慌泼舱,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枷莉,死亡現(xiàn)場離奇詭異娇昙,居然都是意外死亡,警方通過查閱死者的電腦和手機笤妙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門冒掌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蹲盘,你說我怎么就攤上這事宋渔。” “怎么了辜限?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵皇拣,是天一觀的道長。 經(jīng)常有香客問我,道長氧急,這世上最難降的妖魔是什么颗胡? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮吩坝,結(jié)果婚禮上毒姨,老公的妹妹穿的比我還像新娘。我一直安慰自己钉寝,他們只是感情好弧呐,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著嵌纲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逮走。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天茅信,我揣著相機與錄音,去河邊找鬼墓臭。 笑死蘸鲸,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的棚贾。 我是一名探鬼主播榆综,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼铸史!你這毒婦竟也來了鼻疮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤琳轿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后挪哄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡迹炼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年斯入,在試婚紗的時候發(fā)現(xiàn)自己被綠了砂碉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刻两。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖滋迈,靈堂內(nèi)的尸體忽然破棺而出户誓,到底是詐尸還是另有隱情饼灿,我是刑警寧澤厅克,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布赔退,位于F島的核電站证舟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏女责。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一墙基、第九天 我趴在偏房一處隱蔽的房頂上張望刷喜。 院中可真熱鬧残制,春花似錦掖疮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盖腿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背揖庄。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工欠雌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人富俄。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓霍比,卻偏偏與公主長得像幕袱,于是被迫代替她去往敵國和親悠瞬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容