在清单 6 中,我们使用了 AtmGui 的匿名内部子类来覆盖 createTransaction 方法。因为我们只需要覆盖一个简单的方法,所以这是实现我们目标的简明方法。如果我们覆盖多个方法或在许多测试之间共享 AtmGui 子类,那么创建一个完整的(非匿名)成员类是值得的。
我们还使用了实例变量来存储对模仿对象的引用。这是在测试方法和特殊化类之间共享数据的最简单方法。这是可以接受的,因为我们的测试框架不是多线程的或可重入的。(如果它是多线程的或可重入的,则必须用 synchronized 块保护我们自己。)
最后,我们将模仿对象本身定义为测试类的专用内部类 ― 这通常是一种便利的方法,因为将模仿对象就放在使用它的测试代码旁边会更加清楚,又因为内部类有权访问包含它们的类的实例变量。
小心不出大错
因为我们覆盖了工厂方法来编写这个测试,所以其结果是:我们的测试不再包括任何 原始创建代码(现在它在基类的工厂方法内部)。添加确实包括该代码的测试也许是有益的。这与调用基类的工厂方法并断言返回对象具有正确类型一样简单。例如:
2 Transaction t = atm.createTransaction();
3 assertTrue(!(t instanceof MockTransaction));
4
注:相反, assertTrue(t instanceof Transaction) 不能满足,因为 MockTransaction 也是 Transaction 。
厂方法到抽象工厂
此时,您可能很想更进一步并用成熟的抽象工厂对象替换工厂方法,如 Erich Gamma 等人在 设计模式中详细描述的那样。实际上,许多人已经用工厂对象来着手这种方法,而不是用工厂方法 ― 我们以前是这样做的,但很快就放弃了。
将第三种对象类型(角色)引入系统会有一些潜在的缺点:
它增加了复杂性,而没有相应地增加功能。
它会迫使您更改目标对象的公用接口。如果必须传入抽象工厂对象,那么您必须添加一个新的公用构造函数或赋值(mutator)方法。
许多语言对于“工厂”这一概念都附有一些约定,它们会使您误入歧途。例如,在 Java 语言中,工厂通常作为静态方法实现;在这种情况下,这是不合适的。
请记住,本练习的宗旨是使对象更易于 测试。通常,用于可测性的设计可以将对象的 API 推向一种更清晰更模块化的状态。但它会走得太远。测试驱动的设计更改不应该污染原始对象的公用接口。
在 ATM 示例中,对于产品代码, AtmGui 对象始终只产生一种类型的 Transaction 对象(实际类型)。测试代码希望它产生另一种类型的对象(模仿对象)。但强迫公用 API 适应工厂对象或抽象工厂(只因为测试代码要求它这样)是错误的设计。如果产品代码无需实例化该合作者的多个类型,那么添加该功能将使最终的设计不必要地变得难于理解。