关于复合优于继承

646 阅读3分钟

本篇希望从更“简单”的角度考察在《Effective Java》第三版第18条提到的问题的另一种解决思路。

  • 书中举的例子是关于HashSet及其子类的扩展,其中主要的矛盾是在子类维护了子类独有的属性:addCount的同时,又覆写了父类的add和addAll方法,这个覆写使得子类对象在调用子类方法时,如果希望通过super去调用父类逻辑,那么一旦父类的实现中复用了另一个被子类覆写的方法,那其实走的是子类逻辑。
  • 这种做法的问题是:程序员通常希望通过super去调用“纯粹的”父类逻辑,而不希望子类对父类产生任何“影响”,然而,一旦子类使用了覆写(@Overriding),程序员就必须去关心父类的逻辑是否会调用子类的逻辑,这是一种“以下犯上”,子类居然能够反过来影响父类?显然,这是设计上的缺陷。

先看看有问题的代码:

public class InstrumentedHashSet<E> extends HashSet<E> {
    //记录总共的插入次数
    private int addCount;
    @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);
    }
}

显然,当调用addAll方法添加一个有n个元素的集合时,addCount会被写为2n,因为super.addAll的调用在父类中复用了add方法,而这个add方法又恰好被子类覆写了,冗余地调用了n次addCount++。

  • 书中复合优于继承的做法确实是对这个问题的一种解决方案,然而这里尝试从另一个角度考察,认为这个问题的根源是:在父类的方法中维护了子类的信息(super.addAll调用了this.add),super.addAll这个父类方法被子类“污染了”,从这个意义上,子类已经不是一个父类了(childClazz is not a superClazz),已经违反了面向对象的继承(is-a)原则,试问,这个子类还可以调用纯粹的父类的addAll逻辑吗?行不通了!

解决方案:改个方法名

public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount;
    //改个方法名
    public boolean addAndCount(E e) {
        addCount++;
        return super.add(e);
    }
    //改个方法名
    public boolean addAllAndCount(Collection<? extends E> c) {
        addCount+=c.size();
        return super.addAll(c);
    }
}
  • 看到这里你可能很失望,觉得我是在耍流氓,耍无赖,嘤嘤嘤,哭哭哭。

  • 我希望表达的是:

    1. 在面向对象的设计中,在父类的方法中只维护父类的属性,在子类的方法中只维护子类的属性,这时候有小朋友问:老师,如果一个方法两种属性都要维护怎么办?办法是:①把这个方法放到子类中,并且②不要覆写父类,显然,改方法名同时满足了这两个条件。
    1. 对于①很好理解,你总不可能把这个方法放到父类中吧,因为父类中并没有子类的属性。
    1. 对于②,覆写(@Overriding)在某些程度上是对父类的一种“修改”(比如本例对add的覆写影响了父类的addAll行为),这违背了“对修改关闭,对扩展开放”的原则,而在子类中定义全新的方法则是一种“扩展”,父类中并没有这个方法。
    1. 有必要覆写吗?可以不去覆写吗?从类继承的角度说,子类总是比父类更具体,在不改变父类行为的前提下具备新的行为。程序员作为API Caller,在使用子类时,他就应该了解这个子类与其父类有何不同,否则,他为什么不直接使用父类?why?why?why?因此,程序员应当了解这个子类独有的“new method”,哪怕你为这些“new method”构建新的接口,也是必要的。
  • 解决问题的最好办法就是不产生问题,在这里,就是不使用覆写。

欢迎讨论与指正