(16):复合优先于继承

继承是实现代码重用的的有力手段,但它未必是最好的方法。对于普通的具体类进行跨越包边界的继承,则是非常危险的。这里的继承指的是一个类扩展另一个类的继承而非接口继承。

继承的一大缺点在于打破了封装性。子类依赖于超类中特定的功能实现细节。意味着超类如果发生版本变化,子类有可能受到破坏。除非超类是专门为了扩展而设计的。

public class InstrumentedHashSet<E> extends HashSet<E> {  
    // The number of attempted element insertions  
    private int addCount = 0;  
      
    public InstrumentedHashSet() {  
    }  
      
    public InstrumentedHashSet(int initCap, float loadFactor) {  
        super(initCap, loadFactor);  
    }  
      
    @Override  
    public boolean add(E e) {  
        addCount++;  
        return super.add(e);  
    }  
    @Override  
    public boolean addAll(Collections<? extends E> c) {  
        addCount += c.size();  
        return super.addAll(c);  
    }  
    public int getAddCount() {  
        return addCount;  
    }  
}  
我们看上面这个例子。该类继承HashSet 为add()以及addAll()增加了一个记录已添加元素数目的功能。程序乍一看没什么问题。但事实上HashSet 的addAll是依赖于add方法实现的,所以最后又去迭代调用了InstrumentedHashSet中的add方法(因为InstrumentedHashSet重写了HashSet的add方法)。这就导致得到的结果为正确结果的两倍。

我们只要去掉被覆盖的addAll()方法,就可以修正这个子类。虽然这个类可以正常工作。但是它的功能正确性需要信赖于HashSet的addAll方法是在add方法上实现的这一事实,这种自用性(self-use)是实现细节,而非必须要这么实现,不能保证在java平台的所有实现中都保持不变,不能保证随着上发行版本的不同而不发生变化。假设下个版本addAll()不依赖于add()来实现了,那我们这个子类就彻底错了。

导致子类脆弱的还有一个原因是:它们的超类在后续的发行版本中可以获得新的方法,假设一个程序的安全性信赖于这样的事实:所有被插入到某个集合的元素都满足某个先决条件。下面的做法就可以确保这一点:对集合进行子类化,并覆盖所有能够添加元素的方法以便确保在加入每个元素之前它是满足这个先决条件的。如果在后续的发行版本中,超类中没有增加能插入元素的新方法,那么这种方法可以正常工作。然而,一旦超类增加了这样的新方法,则很可能仅仅由于调用了这个未被子类覆盖的新方法,而将不合法的元素添加到子类的实例中。

如果我们仅仅是增加新的方法,而非覆盖现有方法,这样也是有问题的。万一超类增加了一个与我们新增方法同名的方法,仅仅是返回类型不同。那么我们的子类无法通过编译。如果返回类型也相同,相当于我们重写了新增的方法。但我们这个方法是在超类之前就有的,我们无法得知超类这个方法的约定是什么。

使用“复合(composition)”可以解决上述的问题,不用扩展现有的类,而是在新的类中增加一个私有域。通过“转发”来实现与现有类的交互,这样得到手类将会非常稳固。它不信赖于现有类的实现细节。即使现有的类增加了新方法,也不会影响到新类。请看如下的例子

public class InstrumentedSet<E> extends ForwardingSet<E> {  
    private int addCount = 0;  
      
    public InstrumentedSet(Set<E> s) {  
        super(s);  
    }  
      
    @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(c); }  
    public boolean removeAll(Collection<?> c) { return s.removeAll(c); }  
    public boolean retainAll(Collection<?> c) { return s.retainAll(c); }  
    public Object[] toArray() { return s.toArray(); }  
    public <T> T[] toArray(T[] a) { return s.toArray(a); }  
    @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,而是扩展了他的转发类,而在转发类内部,现有Set类是作为它的一个数据域存在的,转发类实现了Set<E>接口,这样他就包括了现有类的基本操作,同时也可能包裹任何Set类型。每个转发动作都直接调用现有类的相应方法并返回相应结果。这样就将信赖于Set的实现细节排除在包裹类之外。有的时候,复合和转发的结合被错误的称为"委托(delegation)"。从技术的角度来说,这不是委托,除非包装对象把自身传递给被包装的对象。
包装类几乎没有什么缺点。需要注意的一点是,包装类不适合用在架设框架上(callback framework),在回调框架中,对象把自身的引用传递给其他的对象,用于后续的调用。因为被包装起来的对象并不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时避开了外面的包装对象。这被称为SELF问题。

总结:

(1)复合与继承的含义:

复合是has a, 继承是is a。
(2)复合与继承的区别:
我们不单单要看到他们形式上的区别,还要看到他们意义上的区别。这就是引出了以下两个问题:
1 为什么使用复合?假如B引用了A,那就说明B的职责需要调用A的职责,但是A的职责并不是B的职责;
2 为什么使用继承?假如B继承了A,那就说明A的职责也是B的职责,只不过B在职责的实现上跟A有所不同。
(3)复合与继承的适用场景:
1 如果A与B负责不同的业务,但是B在实现其业务逻辑时需要调用A的职责,则建议使用复合;
2 如果A与B负责同一个业务,但是A与B在业务实现上有所不同,则建议使用继承。
(4)举例说明
1 预警系统 VS 发送器
预警系统包含生成预警与发送预警这两部分业务,其中发送预警的业务中需要调用发送器的职责,则预警系统与发送器之间应该使用复合;
2 不同的发送器
发送器只有一个发送的职责,但是有不同的实现,如基于邮件和基于短信的,则不同的发送器使用继承。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值