Encapsulate Classes with Creation Method
用创建方法封装类
撰文/Joshua Kerievsky 编译/透明
不同的类隐藏在包的内部、实现了共同的接口,但是客户却直接实例化这些类
将类的构造子设为非公开,并让客户通过超类的创建方法来得到它们的实例
动机
如果客户(client)需要知道每个具体类的存在,那么让客户直接控制这些类的实例化也是个不错的选择。但是,如果客户不想知道这些,又该怎么办呢?如果这些具体类都被放在一个包的内部,并且都实现了同一个接口,而这个接口又不太可能发生变化,那么就应该把这些具体类隐藏起来,让包外部的客户去使用超类公开的创建方法(Creation Method),并通过创建方法得到满足需要的实例。
这样做的动机有几点。首先,可以确保客户只能通过通用的接口来访问不同的类,以确保“分离接口与实现”[GoF];其次,可以将不必为外界所知的类隐藏起来,从而减少一个包的“概念重量”[Bloch];第三,由于对象的创建都通过创建方法来进行,而创建方法的名称可以更好地揭示创建过程的意图,所以可以使实例的创建过程更容易为开发者所理解。
尽管有所有这些好处,还是有人对这个重构持保留态度。对于他们的疑惑,我做出了如下回应:
1、他们不喜欢让超类知道子类的信息,因为这会导致循环依赖——如果你创建了一个新的子类或者对子类的构造子做了增改,就不得不在超类中添加新的创建方法。不过我会告诉他们:这个重构发生的场景是一个包,其中的子类都实现同一个接口。这时他们就会闭嘴了。
2、他们不喜欢在超类中把创建方法和其他实现方法混在一起。我并不担心这个问题,除非创建方法让我难以看清超类的行为——如果真是这样,我就会使用“提取创建类”(Extract Creation Class)的重构。
3、在源代码编译成目标代码之后,他们就不愿意再做这个重构,因为使用目标代码的程序员是不能再添加或修改非公开的类和创建方法的。对此我更加同情。如果包内部的可扩展性的确很重要,而用户又得不到源代码,我就不会把类都封装起来,而是会提供一个创建类(Creation Class)来生成实例。
本文开始处的那幅图简单画出了关系数据库映射过程中的一些对象关系。在采用这个重构之前,程序员们(包括我自己)有时会选错了要实例化的子类,或者用错了参数(比如说,我们可能会调用一个接受Java内建的int类型作为参数的构造子,而真正需要的却是接受Integer对象作为参数的那个构造子)。这个重构会把关于子类的信息封装起来,客户只能从一个意义明确的地方得到子类实例,从而减少了创建错误的机会。
沟通
重复
简化
如果你希望客户代码只通过一个接口与你的类沟通,那么你就必须用代码来反映出自己的要求。公有的构造子没有任何帮助,因为客户通过公有构造子就可以跟子类型耦合在一起。要达到你的要求,就需要把构造子隐藏起来,然后通过超类中的创建方法来生成对象,并且将创建方法的返回类型定为所有子类共同的接口或抽象类。
使用这个重构的时候,重复不是问题。
如果你想让客户只通过一个接口与所有的子类打交道,那么把这些类公开只会把事情搞得更复杂:程序员们会直接实例化子类,并把自己的代码和子类型耦合在一起。这种做法就好象在说:去扩展这些类的接口吧,没有关系。
如果不允许直接实例化这些子类,只通过超类的创建方法提供实例,那么情况就简单多了。
约束
l 你的所有类应该有共同的公有接口。
这是根本的条件,因为在此重构完成之后,所有的客户代码都只能通过这个共同的接口来访问所有这些类的实例。
l 你的所有类应该属于同一个包。
过程
1. 在超类中为每种实例(一个构造子生成的实例称为“一种”)编写创建方法,创建方法的名字应该能清楚地说明自己的意图。创建方法的返回类型应该是所有可创建对象共有的接口类型。让创建方法去调用相应的构造子。
2. 选定一种实例,将所有对应于这种实例的构造子替换成超类中相应的创建方法。
3. 编译、测试。
4. 重复步骤1~3,直到一个类中的每种实例都通过创建方法来创建。
5. 将这个类的构造子声明为非公有(例如protected或者default)。
6. 编译。
7. 重复上面的步骤,直到所有的构造子都变成非公有、所有的实例都可以并且只能通过创建方法来获取。
范例
1、我们从一个比较小的类体系开始,这个类体系被放在descriptors包里面。这些类在对象-关系数据库的映射中起辅助作用,可以把数据库属性转换成实例变量。
package descripors;
public abstract class AttributeDescriptor {
protected AttributeDescriptor(…)
public class BooleanDescriptor extends AttributeDescriptor {
public BooleanDescriptor(…) {
super(…);
}
public class DefaultDescriptor extends AttributeDescriptor {
public DefaultDescriptor(…) {
super(…);
}
public class ReferenceDescriptor extends AttributeDescriptor {
public ReferenceDescriptor(…) {
super(…);
}
抽象类AttributeDescriptor的构造子是protected的,三个子类的构造子则是public的。由于三个子类情况相似,所以我们只需注意DefaultDescriptor就可以了。首先,我们需要识别出DefaultDescriptor的构造子创建的那一种实例,所以请看下面的客户代码:
protected List createAttributeDescriptors() {
Vector result = new Vector();
result.add(new DefaultDescriptor("remoteId", getClass(), Integer.TYPE));
result.add(new DefaultDescriptor("createdDate", getClass(), Date.class));
result.add(new DefaultDescriptor("lastChangedDate", getClass(), Date.class));
result.add(new ReferenceDescriptor("createdBy", getClass(),
User.class,RemoteUser.class));
result.add(new ReferenceDescriptor("lastChangedBy", getClass(),
User.class,RemoteUser.class));
result.add(new DefaultDescriptor("optimisticLockVersion",
getClass(), Integer.TYPE));
return result;
}
我看出来了:DefaultDescriptor被用来表现Integer和Date之间的映射。它还可以用来映射其他类型,但是此刻我只能注意一种实例。所以,我先编写一个创建方法来为Integer对象提供属性描述:
public abstract class AttributeDescriptor {
public static AttributeDescriptor forInteger(...) {
return new DefaultDescriptor(...);
}
我把创建方法的返回类型规定为AttributeDescriptor,因为我希望让客户通过AttributeDescriptor这个接口与其子类进行交流,从而使descriptors包之外的客户无需知道这些子类的存在。
如果你有“测试优先(test-first)”的编程习惯,那么在开始这个重构之前,你应该首先编写一段测试代码,从超类的创建方法中获取AttributeDescriptor的子类实例,并判断所得实例的类型是否正确。
2、现在,客户如果想生成Integer版本的DefaultDescriptor,就必须调用超类中的创建方法:
protected List createAttributeDescriptors() {
List result = new ArrayList();
result.add(AttributeDescriptor.forInteger("remoteId", getClass()));
result.add(new DefaultDescriptor("createdDate", getClass(), Date.class));
result.add(new DefaultDescriptor("lastChangedDate", getClass(), Date.class));
result.add(new ReferenceDescriptor("createdBy", getClass(), User.class,
RemoteUser.class));
result.add(new ReferenceDescriptor("lastChangedBy", getClass(),
User.class,RemoteUser.class));
result.add(AttributeDescriptor.forInteger("optimisticLockVersion", getClass()));
return result;
}
3、编译、测试,确保新的代码运转正常。
4、现在,我继续为DefaultDescriptor的构造子能创建的其他种类的实例编写创建方法。我又得到了另外的两个创建方法:
public abstract class AttributeDescriptor {
public static AttributeDescriptor forInteger(...) {
return new DefaultDescriptor(...);
}
public static AttributeDescriptor forDate(...) {
return new DefaultDescriptor(...);
}
public static AttributeDescriptor forString(...) {
return new DefaultDescriptor(...);
}
5、现在,将DefaultDescriptor的构造子声明为protected:
public class DefaultDescriptor extends AttributeDescriptor {
protected DefaultDescriptor(…) {
super(…);
}
}
6、编译一下,一切尽在掌握。
7、现在,针对AttributeDescriptor的每个子类,重复上面的步骤。完成以后,新的代码应该
u 通过AttributeDescriptor提供对其子类的访问
u 保证客户只能从AttributeDescriptor这个接口获取子类的实例
u 不允许客户直接实例化AttributeDescriptor的任何子类
u 让其他的程序员明白:AttributeDescriptor的子类是不公开的——应该通过超类和通用接口来访问它们。
封装内嵌类
JDK中的java.util.Collections类是“用创建方法封装类”的一个典型范例。这个类的作者Joshua Bloch需要为程序员们提供一种办法来保证Collection、List、Set和Map的不可修改性和(或)同步性。一开始,他很聪明地用Decorator模式来实现。但是,他没有创建公开的java.util.Decorator类并要求程序员们用它来装饰自己的Collections子类。他的做法是:将Decorator定义为Collections类的非公开内嵌类(Inner Class),并在Collections类中设计了一组创建方法,程序员可以通过这些创建方法得到自己需要的、经过装饰的容器。下面是Collections类中的内嵌类和创建方法的设计草图:
请注意,java.util.Collections类甚至还包含了一个小小的内嵌类继承体系,其中所有的类都是非公开的。每个内嵌类都有一个相应的创建方法,这个方法接受一个容器,在其上进行装饰,并用预先定义的常用接口类型(例如List或者Set)返回装饰后的实例。通过这个方案,程序员无须再去了解那么多的类,并且需要的功能也一点不少。同时,java.util.Collections类也是一个创建类(Creation Class)的典型范例(参见“提取创建类”)。