读《Effective java 中文版》(15)
第14条:复合优先于继承
继承是实现代码重用的有力手段,但不适当地使用继承会导致脆弱的软件。
- 在一个包内使用继承是非常安全的,因在那儿子类和超类在同一个程序员的控制之下。
- 对于专门为了继承而设计、且有很好文档说明的类,使用继承也是安全的。
- 对普通的具体类(concret class)时行跨跃包边界的继承,则是非常危险的。
与方法调用不同的是,继承打破了封装性,一个子类依赖于其超类中特定功能的实现细节。超类的实现的变化,则子类可能会被打破。
- 自用性,即超类中不同公有方法的实现时存在调用关系,当子类改写这些方法时,常会导致子类的脆弱
- 超类在它的后续方法中增加了新的方法,如果子类不能及时改写这些方法,异常的数据或操作出现。
- 子类继承超类后,增加了一个新的方法,则当超类在新版中也增加了具有相同原型特征的方法时,可能会出现问题
有一种办法可以避免上述所有问题:新类,不是扩展一个已有类,而是设置一个私有域,它引用这个已有类的一个实例。这种设计被称为“复合(composition)”。新类中的每个实例方法,都可以调用被包含的已有类实例中对应的方法,并返回它的结果,即为“转发方法(forwarding method)”。这样的类比较稳固,这不依赖于已有类的实现细节。一个类的实例都把另一个类的实现包装起来了,则前者的类叫做包装类(wrapper class)。
看一个例子:
//wrapper class - uses composition in place of inheritance
public class InstrumentedSet implements Set{
private final Set s;
private int addCount=0;
public InstrumentedSet(Set s){
this.s=s;
}
public boolean add(Object o){
addCount++;
return s.add();
}
public boolean addAll(Collection c){
addCount+=c.size();
return s.addAll(c);
}
public int getAddCount(){
return addCount;
}
//forwarding methods
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 String toString() {return s.toString(); }
}
上例中,InstrumentedSet类对Set类进行了修饰,增加了计数特性。有时,复合和转发这两项技术的结合被错误地引用为“委托(delegation)”,从技术的角度而言,这不是委托,除非包装对象把自己传递给一个被包装的对象。
包装类几乎没有什么缺点。需要注意的是,包装类不适合用于回调框架(callback framework)中。在回调框架中,对象把自己的引用传递给其它的对象,以便将来调用回来,当它被包装起来以后,它并不知道外面的包装对象的情况,所以它传递一个指向自己的引用(this)时,会造成回调时绕开外面的包装对象的问题。这被称为SELF问题。
只有当子类真正是超类的“子类型(subtype)”时,继承才是合适的。即两者之存在“is-a”的关系。java平台中,也有违反这条规则的地方:如Stack不是向量,所以不应扩展Vector;属性列表不是散列表,所以Properties不应扩展Hashtable。在决定使用复合还是扩展时,还要看一试图扩展的类的API有没有缺陷,如果你愿意传播这些缺陷到自己的API中,则用继承,否则可用复合来设计一个新的API。
Posted by Hilton at February 14, 2004 09:21 PM | TrackBack