一次简单的重构练习记录

在等待马丁大叔的《重构》第二版的艰难日子里,恰巧在一本书里看到了一个 C# 的重构的例子,觉得不错,就转成了 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 类的构造函数中接收一个 AccountType, 在计算积分的时候,根据这个 type, 会有不同的算法,获取的积分也就不同了。

那么我们来看看这个类可以怎么重构呢?(重构之前应该是要在有单元测试的基础上的,这里略去单元测试的代码)。

去掉 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;
    }

}

这样做相当于给了这些 magic numbers 命名,增强了代码的可读性。

用多态替代条件语句

这里的条件语句就是那个 switch 了,目前的积分计算逻辑都是在那一大块 switch 中的代码里,这样随着 AccountType 的种类变多, switch 中的代码有会越来越多。 我们可以通过创建 SilverAccount, GoldAccount, PlatinumAccount 来替代 AccoutType. 这样一来,当新的 AccountType 出现时,只需要新建一个类, 而不需要在 CalculateRewardPoints 方法里增加一个 case 条件,这样更符合开闭原则。

创建不同类型的 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);

}

可以看到,最复杂的 calculateRewardPoints 方法变成了抽象方法,同时构造函数也消失了。 下面就是不同帐户子类的实现(之前的常量也被分散到了各自的子类中了)。

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 类别,只需要新建一个类,然后实现 calculateRewardPoints 方法就可以了。

用工厂方法替代构造函数

刚才我们把 Account 类改成 Abstract 之后,测试代码肯定 broken 了,因为我们没有一个统一的接口来创建各类 Account 了。 之前我们用 Account 的构造函数来区分不同的帐户类别,现在可以使用工厂方法来替代它。

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);
}

这次的改动很小,相较于之前的代码,只是把构造函数换成了一个静态方法,不过这是过渡的方式,接下来我们要把工厂方法抽取出来。

一个新的帐户类型

经过之前的重构,让我们看看当新增一个帐户类型时,需要做哪些改动。 假设我们要新增一个青铜级别(bronze)的帐户。 首先,需要创建一个 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 代码块。 我们可以考虑使用元编程来动态创造 account 实例:

public static Account CreateAccount(String accountType) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    Class c= Class.forName(accountType + "Account");
    return (Account) c.newInstance();
}

不过这种方式太脆弱了,它必须满足下面几个条件:

  1. Account 的类型名必须遵守规范 [Type]Account
  2. Account Type 必须和工厂方法在同一个 assembly 中
  3. 每种 Account Type 必须有一个无参的构造方法

如果有这么多限制的话,那通常说明你的重构有点过了。

代码坏味道:拒绝遗赠

假设我们发现,不是所有的帐户都能够获取积分的,大部份的帐户都是普通帐户,没有积分方面的需求。 那么我们可以创建一个 StandardAccount 的 Account 子类:

public class StandardAccount extends Account {

    protected int calculateRewardPoints(int amount) {
        return 0;
    }
}

在 StandardAccount 中,把 calculateRewardPoints 这个方法直接返回 0, 这是实现的一种方式。 在这个例子中,父类的抽象方法 calculateRewardPoints 对于子类 StandardAccount 来说,是没有意义的, 甚至是一种累赘,因此这种现象可以被称为“拒绝遗赠(refused bequest)”。

使用代理替代继承

继承是一种强耦合关系,从目前的需求来看,标准帐户和其他帐户是不同的两个种类了。 因此我们需要把积分相关的逻辑分离出来,比如创建一个接口 IRewardCard

通过让帐户持有不同的卡片,达到不同的积分记录效果。这里的“持有”就是把 IReardCard 作为 Account 的构造函数参数。

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;
    }

}

只不过构造函数会接收一个 IRewardCard 的实现。 那么我们就以黄金会员卡为例实现 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 去调用 RewardCard 的方法实现积分计算。

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());
    }
}

现在回到之前的问题:如何处理 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 来实现 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());
}

可能有人会觉得这两种实现方式没什么区别,现在是 NullRewardCard 返回 0, 之前是 StardardAccount 返回 0. 但我觉得最重要的是,这样做分离了 balance 和 points, 这样 Account 可以专注于处理 balance 相关的操作, 而 rewardCard 则用于处理 points, 更符合单一职责原则。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容