一次简单的重构练习记录

在等待马丁大叔的《重构》第二版的艰难日子里,恰巧在一本书里看到了一个 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, 更符合单一职责原则。