淘先锋技术网

首页 1 2 3 4 5 6 7

在包的内部使用继承是非常安全的。在那里,子类和超类的实现都处在同一个程序员的控制之下。对于专门为了继承而设计、并且具有很好地文档说明的类来说,使用继承也是非常安全的。

与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使他的代码完全没有改变。因此,子类必须要跟着其超类的更新而演变,除非超类是专门为了扩展而设计的,并且具有很好地文档说明。

不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计被称作“复合(composition)”,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这杯称为转发(forwarding),新类中的方法被称为转发方法(forwarding method)。这样得到的类将会非常稳固,它不依赖于现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。为了进行更具体的说明,请看下面的例子,他用复合/转发的方法来代替InstrumentedHashSet类。注意这个实现分为两部分:类本身和可重用的转发类(forwarding class),包含了所有的转发方法,没有其他方法。

// Wrapper class - uses composition in place of inheritance

public class InstrumentedSet<E> extends ForwardingSet<E> {

private int addCount = 0;

public InstrumentedSet(Set<E> s) {

super(e);

}

@Override

public boolean add(E e) {

addCount ++ ;

return super.add(e);

}

@Override

public boolean addAll(Collection<? extends E> c) {

addCount += c.size();

return super.addAll(c);

}

public int getAddCount() {

return addCount();

}

}

// Reusable forwarding class

public class forwardingSet<E> implements Set<E> {

private final Set<E> s;

public ForwardingSet(Set<E> s) { this.s = s; }


public void clear() { s.clear(); }

public boolean contains(Object o) { return s.contains(o); }

public boolean isEmpty() { return s.isEmpty(); }

public int size() { return s.size(); }

public Iterator<E> iterator() { return s.iterator(); }

public boolean add(E e) { return s.add(e); }

public boolean remove(Object o)  { return s.remove(o); }

public boolean containsAll(Collection<?> c) { return s.containsAll(c); }

public boolean addAll(Collection<? extends E> c) {  return s.addAll(); }

public boolean retainAll(Collection<?> c) { return s.retainAll(c); }

public Object[] toArray() { return s.toArray(); }

@Override 

public boolean equals(Object o) {  return s.equals(o); }

@Override

public int hashCode() { return s.hashCode(); }

@Override 

public String toString() { return s.toString(); }

Set接口的存在使得INstrumentedSet类的设计成为可能,因为Set接口保存了HashSet类的功能特性。除了获得健壮性之外,这种设计也带来了格外的灵活性。InstrumentedSet类实现了Set接口,并且拥有单个构造器,它的参数也是Set类型。从本质上讲,这个类把一个Set转变成了另一个Set,同时增加了计数的功能。前面提到的基于继承的方法只适用于单个具体的类,并且对于超类中所支持的每个构造器都要求有一个单独的构造器,与此不同的是,这里的包装类(wrapper class)可以被用来包装任何Set实现,并且可以结合任何先前存在的构造器一起工作。例如:

Set<Date> s = new InstrumentedSet<Date>(new TreeSet<Date>(cmp));

Set<E> s2 = new InstrumentedSet<E>(new HashSet<E>(capacity));

InstrumentedSet甚至也可以用来临时替换一个原本没有计数特性的Set实例:

static void walk(Set<Dog> dogs) {

InstrumentedSet<Dog> iDogs = new InstrumentedSet<Dog>(dogs);

... // within this method use iDogs instead of dogs

}

因为每一个InstrumentedSet实例都把另一个Set实例包装起来了,所以InstrumentedSet类被称做包装类(wrapper class)。这也正是Decorator模式,因为InstrumentedSet类对一个集合进行了修饰,为他增加了计数特性。有时候,复合和转发的结合也被错误的称为“委托(delegation)”。从技术的角度而言,这不是委托,除非包装对象把自身传递给被包装的对象。

包装类几乎没有什么缺点。需要注意的一点是,包装类不合适用在回调框架(callback framework)中;在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用(“回调”)。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时避开了外面的包装对象。这被称为SELF问题。有些人担心转发方法调用所带来的性能影响,或者包装对象导致的内存占用。在实践中,这两者都不会造成很大的影响。编写转发方法倒是有些琐碎,但是只需要给每个接口编写一次构造器,转发类则可以通过包含接口的包替你提供。

只有当子类真正是超类的子类型(subtype)时,才适合继承。换句话说,对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该扩展A。如果你打算让B扩展A,就应该问问自己:每个B确实也是A吗?如果你不能够确定这个问题的答案是肯定的,那么B就不应该扩展A。如果答案是否定的,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的、叫简单的API:A本质上不是B的一部分,只是他的实现细节而已。

在Java平台类库中,有许多明显违反这条原则的地方。例如,栈(stack)并不是向量(vector),所以Stack不应该扩展Vector。同样的,属性列表也不是散列表,所以Properties不应该扩展Hashtable。在这两种情况下,复合模式才是恰当的。

如果在适合于使用复合的地方使用了继承,则会不必要地暴露实现细节。这样得到的API会把你限制在原始的实现上,永远限定了类的性能。更为严重的说,由于暴露了内部的细节,客户端就有可能直接访问这些内部细节。这样至少会导致语义上的混淆。

在决定使用继承而不是复合之前,还应该问自己最后一组问题。对于你正试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把那些缺陷传播到类的API中?继承机制会把超类API中的所有缺陷传递到子类中,而复合则允许设计新的API来隐藏这些缺陷。

简而言之,继承的功能非常强大,但是也存在诸多问题。因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性(fragility)。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当地接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。