7

为什么更推荐使用组合而非继承关系? - JAVA旭阳

 1 year ago
source link: https://www.cnblogs.com/alvinscript/p/17003457.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近在看公司项目的代码,看到了大量的继承体系,而且还是继承了多层,维护、阅读都十分的困难。在查阅了一些资料以后,包括《Effective Java》一书中的第16条提到“组合优先于继承”。那继承到底会暴露什么问题呢?为什么更推荐优先使用组合呢?

欢迎关注微信公众号「JAVA旭阳」交流和学习

继承带来的问题

老实讲,项目中为什么大量使用继承,估计初版设计的人是想实现代码的复用,但是的确带来不少的问题。

继承是面向对象重要特性之一,语义上是表达 is-a的关系,但是它会破坏封装性。我们举个例子:

假设我们要设计一个关于鸟的类。我们将“鸟类”这样一个抽象的事物概念,定义为一个抽象类 AbstractBird,默认有eat吃东西的行为。所有更细分的鸟,比如麻雀、鸽子、鸵鸟等,都继承这个抽象类。

public class AbstractBird { 
 	//... 省略其他属性和方法... 
 	public void eat() { //... }
}

// 鸵鸟
public class Ostrich extends AbstractBird { 

}

但是,这时候搞不清楚情况的人根据需求给AbstractBird添加一个fly()的行为。但是对于鸵鸟这个子类来说,并不会飞,你如果不做任何处理,相当于让鸵鸟有了飞翔的功能,不符合设计。聪明的你想到了,那就重写以下吧,抛出一个异常,如下所示:

public class AbstractBird { 
 	//... 省略其他属性和方法... 
 	public void eat() { //... }

    public void fly() { //... }
}

// 鸵鸟
public class Ostrich extends AbstractBird { 
 //... 省略其他属性和方法... 
 public void fly() { 
     throw new UnSupportedMethodException("I can't fly.'");  
 }  
}

这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 fly() 方法,抛出异常。而且真正好的设计,对于鸵鸟和企鹅来说,就不应该暴露给他们fly()这种不该暴露的接口,增加外部调用的负担。

这里只提到了fly(),如果还有下蛋egg()、唱歌sing()这么多行为,总不能都冗杂在父类里吧。关键像我们的项目同事,基本上把所有的类都写到了父类中,真的特别难以维护。

小结一下继承带来的问题:

  1. 子类继承了父类所有的行为,会让子类无意的暴露的不必要的接口,破坏封装性。
  2. 如果继承层级比较多,那么代码的复杂度、可阅读型就可想而知的难了。
  3. 另外一个点,就是非常不好做单元测试。

针对于这种问题,组合能怎么解决呢?

组合的好处

组合,顾名思意,就是把另外一个对象做成当前这个对象的一部分,是组成我的一部分,它也能很好的实现代码的复用,语义上表达的是has-a的意思,我有xxx的能力,我有xxx的功能。

那我们看看针对上面的例子,用组合的方式该如何实现呢?

public interface Eatable {
    void eat();
} 
public interface Flyable { 
    void fly(); 
} 

public class EatAbility implements Eatable { 
    @Override 
    public void eat() {
        System.out.println("I can eat");
    } 
}  // 省

public class FlyAbility implements Flyable { 
    @Override 
    public void fly() {
        System.out.println("I can fly");
    } 
}  // 省
  1. 组合鸵鸟类
public class Ostrich implements Eatable {// 鸵鸟
 	private Eatable eatable = new EatAbility(); // 组合
 	//... 省略其他属性和方法... 
	@Override 
    public void eat() { 
        eatable.eat(); // 委托
	} 
}

你看对于鸵鸟这个子类来说,只暴露了它有的能力,那就是eat,没有暴露fly的接口。

从理论上讲,通过组合、接口、委托三个技术手段,我们完全可以替换掉继承,在项目中不用或者少用继承关系,特别是一些复杂的继承关系。

继承真的无用武之地了?

既然面向对象中有继承这玩意,说明它并非一无是处的。

如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。

除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。

不知道大家项目中继承用的多吗?其实在JDK中就有许多违反这条原则的地方,比如栈Stack类并不是Vector,不应该有继承关系,但是实际上就是继承自Vector。不管如何,在项目中决定使用继承而不是组合前,一定要考虑清楚,子类是否真的是父类的子类型?以后父类会不会经常变动的可能?父类的某些API是否存在缺陷,如果有的话也会随着子类扩散出去。

欢迎关注微信公众号「JAVA旭阳」交流和学习
更多学习资料请移步:程序员成神之路


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK