在等待马丁大叔的《重构》第二版的艰难日子里,恰巧在一本书里看到了一个 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();
}
不过这种方式太脆弱了,它必须满足下面几个条件:
- Account 的类型名必须遵守规范 [Type]Account
- Account Type 必须和工厂方法在同一个 assembly 中
- 每种 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, 更符合单一职责原则。