为什么更推荐使用组合而非继承关系?
theme: smartblue
如果本文对你有帮助的话,救救孩子吧,帮忙投个票,投票通道 ,不胜感激呀~~
前言
最近在看公司项目的代码,看到了大量的继承体系,而且还是继承了多层,维护、阅读都十分的困难。在查阅了一些资料以后,包括《Effective Java》一书中的第16条提到“组合优先于继承”。那继承到底会暴露什么问题呢?为什么更推荐优先使用组合呢?
继承带来的问题
老实讲,项目中为什么大量使用继承,估计初版设计的人是想实现代码的复用,但是的确带来不少的问题。
继承是面向对象重要特性之一,语义上是表达 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()
这么多行为,总不能都冗杂在父类里吧。关键像我们的项目同事,基本上把所有的类都写到了父类中,真的特别难以维护。
小结一下继承带来的问题:
- 子类继承了父类所有的行为,会让子类无意的暴露的不必要的接口,破坏封装性。
- 如果继承层级比较多,那么代码的复杂度、可阅读型就可想而知的难了。
- 另外一个点,就是非常不好做单元测试。
针对于这种问题,组合能怎么解决呢?
组合的好处
组合,顾名思意,就是把另外一个对象做成当前这个对象的一部分,是组成我的一部分,它也能很好的实现代码的复用,语义上表达的是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"); } } // 省 ```
- 组合鸵鸟类
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是否存在缺陷,如果有的话也会随着子类扩散出去。
如果本文对你有帮助的话,救救孩子吧,帮忙投个票,投票通道 ,不胜感激呀~~
- Java7到Java17, Switch语句进化史
- 乐观锁思想在JAVA中的实现——CAS
- 一步步带你设计MySQL索引数据结构
- 我总结了写出高质量代码的12条建议
- 工作这么多年,我总结的数据传输对象 (DTO) 的最佳实践
- Spring项目中用了这种解耦模式,经理对我刮目相看
- 大数据HDFS凭啥能存下百亿数据?
- 5个接口性能提升的通用技巧
- 你的哪些SQL慢?看看MySQL慢查询日志吧
- 90%的Java开发人员都会犯的5个错误
- 丧心病狂,竟有Thread.sleep(0)这种写法?
- 为什么更推荐使用组合而非继承关系?
- 一个30岁程序员的觉醒和进击
- 推荐8个提高工作效率的IntelliJ插件
- 公司的这种打包启动方式,我简直惊呆了
- 告别丑陋判空,一个Optional类搞定
- 你不知道的Map家族中的那些冷门容器
- SpringBoot 2.x整合Log4j2日志
- SpringBoot应用自定义logback日志
- 你确定懂了Java中的序列化机制吗