前言最近在看公司项目的代码,看到大量的继承体系,而且还是多层继承,维护和阅读难度很大。查阅了一些资料,包括该书的第16条《Effective Java》提到“组合优先于继承”。那么继承会暴露出哪些问题呢?为什么更推荐先使用组合?继承带来的问题说实话,为什么项目中会广泛使用继承呢?估计设计第一个版本的人是想实现代码复用,但是确实带来了很多问题。继承是面向对象的重要特征之一。在语义上表达了is-a的关系,但是会破坏封装性。举个例子:假设我们要设计一个关于鸟类的类。我们将“鸟”这个抽象概念定义为一个抽象类AbstractBird,它默认有吃的行为。所有细分的鸟类,如麻雀、鸽子、鸵鸟等,都继承了这个抽象类。publicclassAbstractBird{//...省略其他属性和方法...publicvoideat(){//...}}//ostrichpublicclassOstrichextendsAbstractBird{}然而,不明白情况的人此时根据需要在AbstractBird中添加一个fly()行为。但是对于鸵鸟这个亚纲来说,它是不会飞的。如果什么都不做,就相当于让鸵鸟拥有了飞行的功能,不符合设计。聪明的你想到了,于是改写如下并抛出异常,如下所示:publicclassAbstractBird{//...省略其他属性和方法...publicvoideat(){//...}publicvoidfly(){//...}}//ostrichpublicclassOstrichextendsAbstractBird{//...省略其他属性和方法...publicvoidfly(){thrownewUnSupportedMethodException("我可以'不要飞。'");这种设计思路虽然可以解决问题,但是不够美观。因为除了鸵鸟,还有很多不会飞的鸟类,比如企鹅。对于这些不会飞的鸟,我们都需要重写fly()方法抛出异常。而真正好的设计,对于鸵鸟和企鹅来说,不应该暴露fly(),不应该暴露,增加外部调用的负担。这里只提到fly()。如果下蛋()、唱歌()等这么多行为,在父类中不可能都是多余的。关键像我们的项目同事,基本上把所有的类都写到父类里面,维护起来真的很难。总结一下继承带来的问题:子类继承了父类的所有行为,会使得子类无意中暴露了不必要的接口,破坏了封装。如果继承层次很多,代码的复杂度和可读性将难以想象。还有一点就是单元测试做起来非常困难。组合如何解决这个问题?组合的好处组合,顾名思义,就是让另一个对象成为当前对象的一部分,也就是我的一部分。也可以很好的实现代码重用,语义上表达了has-a的意思。我有xxx的能力,我有xxx的功能。那我们就看看上面的例子,组合的方式是如何实现的呢?定义接口publicinterfaceEatable{voideat();}publicinterfaceFlyable{voidfly();}publicclassEatAbilityimplementsEatable{@Overridepublicvoideat(){System.out.println("我可以吃");}}//省公共类FlyAbility实现Flyable{@Overridepublicvoidfly(){System.out.println("Icanfly");}}//省组合鸵鸟publicclassOstrichimplementsEatable{//ostrichprivateEatableeatable=newEatAbility();//组合//...省略其他属性和方法...@Overridepublicvoideat(){eatable.eat();//delegate}}你看,对于ostrich的子类来说,也就是说只暴露了它拥有的能力,也就是eat,并没有暴露fly的接口。从理论上讲,通过组合、接口、委托这三种技术手段,我们可以完全替代继承,在项目中少用或不用继承关系,尤其是一些复杂的继承关系。继承真的没用吗?面向对象中既然有继承,就说明不是没有用。如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如最多有两层继承关系),继承关系不复杂,就可以大胆使用继承。反之,系统越不稳定,继承层次越深,继承关系越复杂,我们就尽量用组合代替继承。此外,还有一些设计模式以固定的方式使用继承或组合。例如装饰者模式(decoratorpattern)、策略模式(strategypattern)、复合模式(compositepattern)等都使用了组合关系,而模板模式(templatepattern)则使用了继承关系。总结不知道大家在项目中是否大量使用了继承?事实上,JDK中有很多违反这个原则的地方。比如Stack类不是Vector,不应该有继承关系,但它实际上继承自Vector。不管怎样,在决定在项目中使用继承而不是组合之前,你必须考虑子类是否真的是父类的子类型?以后父类会不会经常变化?父类的某些API是否存在缺陷,如果有,会随着子类扩散出去。
