角色
该设计引入了由系统中的对象扮演的下列角色:
· 目标对象:正在测试的对象
· 合作者对象:由目标对象创建或获取的对象
· 模仿对象:遵循模仿对象模式的合作者的子类(或实现)
· 特殊化对象:覆盖创建方法以返回模仿对象而不是合作者的目标的子类
技巧
重构由许多小的技术性步骤组成。这些步骤统称为技巧。如果您象按照食谱那样严格遵循这些技术,那么您在学习重构时应该没有太大的麻烦。
标识创建或获取合作者的代码的所有出现。
将抽取方法重构应用于这个创建代码,创建工厂方法(在Fowler书籍的第110页中讨论;有关更多信息,请参阅参考资料一节)。
确保目标对象及其子类可以访问工厂方法。(在 Java 语言中,使用 protected 关键字)。
在测试代码中,创建模仿对象且实现与合作者相同的接口。
在测试代码中,创建扩展(专用于)目标对象的特殊化对象。
在特殊化对象中,覆盖创建方法以返回为测试提供的模仿对象。
可选的:创建单元测试以确保原始目标对象的工厂方法仍返回正确的非模仿对象。
示例:ATM
设想您正在编写用于银行自动柜员机(Automatic Teller Machine)的测试。其中一个测试可能类似于清单 2:
清单 2. 初始单元测试,在模仿对象引入之前:
public void testCheckingWithdrawal() {
float startingBalance = balanceForTestCheckingAclearcase/" target="_blank" >ccount();
AtmGui atm = new AtmGui();
insertCardAndInputPin(atm);
atm.pressButton("Withdraw");
atm.pressButton("Checking");
atm.pressButtons("1", "0", "0", "0", "0");
assertContains("$100.00", atm.getDisplayContents());
atm.pressButton("Continue");
assertEquals(startingBalance - 100,
balanceForTestCheckingAccount());
}
另外,AtmGui 类内部的匹配代码可能类似于清单 3:
清单 3. 产品代码,在重构之前:
private Status doWithdrawal(Account account, float amount) {
Transaction transaction = new Transaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.successful()) {
dispense(amount);
}
return transaction.getStatus();
}
该方法将起作用,遗憾的是,它有一个副作用:支票帐户余额比测试开始时少,这使得其它测试变得更困难。有一些解决这种困难的方法,但它们都会增加测试的复杂性。更糟的是,该方法还需要对管理货币的系统进行三次往返。
要修正这个问题,第一步是重构 AtmGui 以允许我们用模仿事务替换实际事务,如清单 4 中所示(比较粗体的源代码以查看我们正在更改什么):
清单 4. 重构
AtmGui private Status doWithdrawal(Account account, float amount) {
Transaction transaction = createTransaction();
transaction.setSourceAccount(account);
transaction.setDestAccount(myCashAccount());
transaction.setAmount(amount);
transaction.process();
if (transaction.successful()) {
dispense(amount);
}
return transaction.getStatus();
}
protected Transaction createTransaction() {
return new Transaction();
}