Replace Multiple Constructors with Creation Methods
(用创建方法取代多个构造子)
撰文/Joshua Kerievsky 编译/透明
如果一个类中有多个构造子,在开发过程中将难以决定究竟选择哪一个。
用能表现意图的创建方法(Creation Method)返回对象实例,从而取代构造子的作用。
动机
某些语言允许你用自己喜欢的任何方式为自己的构造子命名,而不用管类的名字。另一些语言(例如C++和Java)则不允许这样做:每个构造子都必须按照所属的类的名字来命名。如果只有一个构造子,不成问题;但是如果拥有多个构造子,程序员就必须去了解构造子期望的参数、观察构造子的代码,这样才能正确选择自己要使用的构造子。这有什么毛病?毛病太多了。多个构造子根本无法有效描述意图。拥有的构造子越多,程序员就越容易选择错误。而且,程序员不得不去选择使用哪个构造子,这降低了开发的速度,而且调用构造子的代码经常不能有效的与构造出来的对象交流。
如果你觉得这听起来很糟糕,还有更糟糕的呢。随着系统日趋成熟,程序员们经常会加入越来越多的构造子,而不去检查以前的构造子是否仍在使用。不再使用的构造子仍然留在类中,这增加了类的静态负载,只会让类变得愈加臃肿而复杂。成熟的软件系统中往往充斥着这种“死构造子”,因为程序员没有快速而简单的途径来识别某个构造子是否仍被调用:IDE帮不了他们,判断某个方法确切的调用者所需的搜索语句又实在太麻烦。另一方面,如果对象创建调用主要通过一个特定名称的方法来进行,例如createTermLoad()或者createRevolver(),找到这些方法所有的调用者是轻而易举的。
那么,我们的同行们通常把创建对象的方法叫做什么?许多人都会回答“工厂方法(Factory Method)”,这是因为那本经典著作《设计模式》[GoF]这样称呼一个创建型模式。但是,所有创建对象的方法真的都是工厂方法吗?如果给“工厂方法”这个术语一个更宽的定义:只要创建对象的方法都叫工厂方法,那么答案肯定是“Yes”。但是按照这个创建型模式(Factory Method模式)的作者撰写它的方式来看,很明显并非所有创建对象的方法都能提供真正的工厂方法所提供的那种松耦合。因此,为了在讨论与对象创建相关的设计和重构时保证大家的清醒,我用“创建方法(Creation Method)”这个术语来表示“一个创建对象的方法”。也就是说:工厂方法都是创建方法,但相反则未必。这还意味着:在任何Martin Fowler或Joshua Bloch使用“工厂方法”这个术语(分别在他们精彩的书《Refactoring》[Fowler]和《Effective Java》[Bloch]中)时,你都可以代之以“创建方法”。
交流
重复
简化
多个构造子不是一种好的交流形式——很明显,通过能揭示意图的创建方法提供对实例的访问,这是一种好的交流形式。
没有直接的重复,只是有许多看上去几乎完全一样的构造子。
指出应该调用哪个构造子并不是一件容易的事。通过能揭示意图的创建方法来构造不同类型的对象,这个问题就简单多了。
过程
1. 识别出拥有多个构造子的类。这些构造子通常有一大堆参数,而这就让开发者需要获得一个实例的时候更加迷惑了。
2. 识别出catch-all构造子,或者用Chain Constructor(见第8期《非程序员》)创建一个catch-all构造子。如果catch-all构造子的可见性是public,将它改为private或protected。
3. 针对每个构造子所能构造的那种对象,创建一个能够揭示意图的创建方法。测试,确保每个创建方法都返回正确的对象,确定每个创建方法都被客户代码调用到了(如果某个创建方法没有使用者,将它删掉;到需要它的时候再放回来)。
4. 把所有对构造子的调用都换成对相应创建方法的调用。这需要费些力气,但是可以使客户代码的易读性大大提高。
范例
1、在下面的示例代码段中,我们拥有一个Loan类,其中有多个构造子,分别表示定期贷款(Term Loan)、活期贷款(Revolver)和定活两便贷款(RCTL)。[1]
public class Loan {
private static String TERM_LOAN = “TL”;
private static String REVOLVER = “RC”;
private static String RCTL = “RCTL”;
private String type;
private CapitalStrategy strategy;
private float notional;
private float outstanding;
private int customerRating;
private Date maturity;
private Date expiry;
public Loan(float notional, float outstanding, int customerRating, Date expiry) {
this(TERM_LOAN, new TermROC(), notional, outstanding,
customerRating, expiry, null);
}
public Loan(float notional, float outstanding, int customerRating, Date expiry,
Date maturity) {
this(RCTL, new RevolvingTermROC(), notional, outstanding, customerRating,
expiry, maturity);
}
public Loan(CapitalStrategy strategy, float notional, float outstanding,
int customerRating, Date expiry, Date maturity) {
this(RCTL, strategy, notional, outstanding, customerRating,
expiry, maturity);
}
public Loan(String type, CapitalStrategy strategy, float notional,
float outstanding, int customerRating, Date expiry) {
this(type, strategy, notional, outstanding, customerRating, expiry, null);
}
public Loan(String type, CapitalStrategy strategy, float notional,
float outstanding, int customerRating, Date expiry, Date maturity) {
this.type = type;
this.strategy = strategy;
this.notional = notional;
this.outstanding = outstanding;
this.customerRating = customerRating;
this.expiry = expiry;
if (RCTL.equals(type))
this.maturity = maturity;
}
}
这个类有5个构造子,最后的一个是catch-all构造子。如果只是用看的,你很难知道究竟哪一个构造子创建哪种对象。我碰巧知道RCTL既需要终止日期也需要到期日期,所以我知道要创建RCTL对象必须调用让我传进这两个日期的构造子。但是使用这个类的其他程序员是否也知道这一点呢?如果他们不知道,构造子能与他们充分交流吗?当然了,费点力气,他们也能找出这些规律。但是他们本不应该费这些力气来获取所需的Loan对象的。
在继续进行重构之前,我需要知道上面这些构造子还做出了什么其他的假设。这里有一个比较重要的:如果调用第一个构造子,你会得到一个定期贷款对象而不是活期贷款对象。如果你需要的是活期贷款对象,请调用最后两个构造子,它们会要求你传递贷款类型参数进去。唔……这个类所有的用户都知道这一点吗?我很怀疑。或者他们是否一定会遇到一些非常讨厌的bug,然后才能学到这些?
2、下一步的任务是要识别出Loan类的catch-all构造子。很简单——接收参数最多的那一个就是:
public Loan(String type, CapitalStrategy strategy, float notional, float outstanding,
int customerRating, Date expiry, Date maturity) {
this.type = type;
this.strategy = strategy;
this notional = notional;
this.outstanding = outstanding;
this.customerRating = customerRating;
this.expiry = expiry;
if (RCTL.equals(type)
this.maturity = maturity;
}
把这个构造子声明为protected:
protected Loan(String type, CapitalStrategy strategy, float notional, float outstanding,
int customerRating, Date expiry, Date maturity)
3、然后,我们必须判断出Loan类的每个构造子创建的对象种类。在这个例子中,有下列的种类:
l 使用默认首选策略的定期贷款对象。
l 使用定制首选策略的定期贷款对象。
l 使用默认首选策略的活期贷款对象。
l 使用定制首选策略的活期贷款对象。
l 使用默认首选策略的RTCL对象。
l 使用定制首选策略的RTCL对象。
首先我要为新的创建方法编写测试,让它用默认定期贷款首选策略返回一个定期贷款对象。
public void testTermLoan() {
String custRating = 2;
Date expiry = createDate(2001, Calendar.NOVEMBER, 20);
Loan loan = Loan.newTermLoan(1000f, 250f, CUSTOMER_RATING, expiry);
assertNotNull(loan);
assertEquals(Loan.TERM_LOAN, loan.getType());
}
这个测试无法编译运行,直到我为Loan类加入下面这个静态方法:
public class Loan...
static Loan newTermLoan(float notional, float outstanding, int customerRating,
Date expiry) {
return new Loan(TERM_LOAN, new TermROC(), notional, outstanding,
customerRating, expiry, null);
}
请注意看这个方法如何代理步骤1中识别出的protected的catch-all构造子。我还创建5个类似的测试和5个附加的能够揭示意图的创建方法,对应于剩下的5种对象。在这项工作完成之后,Loan类已经没有public的构造子了。重构完成后的Loan类看起来象这样:
public class Loan {
private static String TERM_LOAN = “TL”;
private static String REVOLVER = “RC”;
private static String RCTL = “RCTL”;
private String type;
private CapitalStrategy strategy;
private float notional;
private float outstanding;
private int customerRating;
private Date maturity;
private Date expiry;
protected Loan(String type, CapitalStrategy strategy, float notional,
float outstanding, int customerRating, Date expiry, Date maturity) {
this.type = type;
this.strategy = strategy;
this notional = notional;
this.outstanding = outstanding;
this.customerRating = customerRating;
this.expiry = expiry;
if (RCTL.equals(type)
this.maturity = maturity;
}
static Loan newTermLoan(float notional, float outstanding, int customerRating,
Date expiry) {
return new Loan(TERM_LOAN, new TermROC(), notional, outstanding, customerRating,
expiry, null);
}
static Loan newTermWithStrategy(CapitalStrategy strategy, float notional,
float outstanding, int customerRating, Date expiry) {
return new Loan(TERM_LOAN, strategy, new TermROC(), notional, outstanding,
customerRating, expiry, null);
}
static Loan newRevolver(float notional, float outstanding, int customerRating,
Date expiry) {
return new Loan(REVOLVER, new RevolverROC(), notional, outstanding,
customerRating, expiry, null);
}
static Loan newRevolverWithStrategy(CapitalStrategy strategy, float notional,
float outstanding, int customerRating, Date expiry) {
return new Loan(REVOLVER, strategy, new RevolverROC(), notional, outstanding,
customerRating, expiry, null);
}
static Loan newRCTL(float notional, float outstanding, int customerRating,
Date expiry, Date maturity) {
return new Loan(RCTL, new RCTLROC(), notional, outstanding,
customerRating, expiry, maturity);
}
static Loan newRCTLWithStrategy(CapitalStrategy strategy, float notional,
float outstanding, int customerRating, Date expiry, Date maturity) {
return new Loan(RCTL, strategy, new RevolverROC(), notional, outstanding,
customerRating, expiry, maturity);
}
}
现在,如何获取需要的Loan实例就很清楚了——你只需要看看自己需要哪种对象,然后调用合适的方法就行了。新的创建方法仍然有相当多的参数。Introduce Parameter Object[Fowler]是一个可以帮助你减少传递给方法的参数的重构。
参数化创建方法
在考虑实现这个重构的时候,你可能会在脑子里算一笔帐:为了支持你的类提供的各种对象配置,需要大约50个创建方法。编写50个方法,这听起来可不是件有趣的事,所以你可能会决定不做这个重构。但是,也有办法对付这种情形。首先,你不需要为每种对象配置都生成一个创建方法:你可以为几种最常见的配置编写创建方法,并留下一些public的构造子来处理剩下的情况。另外,经常可以用参数来减少创建方法的数量——我们把它们叫做参数化创建方法。比如说,一个简单的Apple类可以根据以下条件实例化:
l 根据苹果的品种。
l 根据苹果的产地。
l 根据苹果的颜色。
l 有籽还是无籽。
l 剥好皮的还是没剥皮的。
这些选项表现出了几种不同类型的苹果,尽管它们没有被显式定义为Apple类的子类。为了获取你所需要的Apple实例,你必须调用恰当的构造子。但是对应于很多类型的苹果,Apple类可能有很多构造子:
public Apple(AppleFamily family, Color color) {
this(family, color, Country.USA, true, false);
}
public Apple(AppleFamily family, Color color, Country country) {
this(family, color, country, true, false);
}
public Apple(AppleFamily family, Color color, boolean hasSeeds) {
this(family, color, Country.USA, hasSeeds, false);
}
public Apple(AppleFamily family, Color color, Country country, boolean hasSeeds) {
this(family, color, country, hasSeeds, false);
}
public Apple(AppleFamily family, Color color, Country country,
boolean hasSeeds, boolean isPeeled) {
this.family = family;
this.color = color;
this.country = country;
this.hasSeeds = hasSeeds;
this.isPeeled = isPeeled;
}
正如我们前面提到过的,这么多的构造子只会让Apple类的使用更加困难。为了改善Apple类的可用性,而又不编写大量的创建方法,我们可以识别出最常见的苹果种类,并为它们准备创建方法:
public static Apple createSeedlessAmericanMacintosh();
public static Apple createSeedlessGrannySmith();
public static Apple createSeededAsianGoldenDelicious();
这些创建方法不能完全替代public的构造子,但是可以起到补充的作用,并且有可能减少构造子的数量。但是,因为上面的创建方法不是参数化的,随着时间的流逝,它们的数量很可能翻番,变成许多许多的创建方法,这也同样会让Apple类的使用者难以选择。因此,在面临如此多的可能性时,编写参数化创建方法通常都是有意义的:
public static Apple createSeedlessMacintosh(Country c);
public static Apple createGoldenDelicious(Country c);
参考文献
l [Bloch] Bloch, Joshua. Effective Java. Addison-Wesley, 2001.
l [Fowler] Fowler, Martin. Refactoring: Improving the Design of Existing Code. Addison-Wesley.
l [GOF] Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object Oriented Software. Reading, Mass.: Addison-Wesley, 1995.
[1] 译注:这里的Term Loan, Revolver, RCTL这三个词我都不太理解,请大家将就着吧。