引言:
面向对象中, 有一条非常经典的设计原则, 叫: 组合优于继承, 多用组合少用继承
为什么不推荐使用继承?
组合相比继承有哪些优势?
如何判断改用组合还是继承?
一. 为什么不推荐使用继承?
继承是面向对象4大特性之一, 表示类之间的is-a关系, 解决代码复用问题.
如果继承层次过深, 继承关系复杂, 会影响到代码的可读性和可维护性.
举例子:
我们设计关于鸟的类, 鸟类是一个抽象的概念, 则定义个抽象类: AbstractBrid, 然后有很多子类, 比如: 麻雀, 乌鸦, 鸽子都继承这个抽象类
鸟会飞, 则抽象类中定义一个fly()方法
public class AbstractBird {
//...省略其他属性和方法...
public void fly() { //... }
}
public class Ostrich extends AbstractBird { //鸵鸟
//...省略其他属性和方法...
public void fly() {
throw new UnSupportedMethodException("I can't fly.'");
}
}
可是也有的鸟是不会飞的, 比如: 鸵鸟. 它不应该有fly这个功能, 怎么办呢? 我们能想到的就是在鸵鸟这个类: Ostrich类中重写fly方法时, 抛出异常即可.
可是不会飞的鸟有很多, 比如企鹅, 这些子类都需要重写fly方法, 然后抛出异常. 这会造成2方面问题:
- 无形中增加了编码工作量;
- 违背了迪米特法则, 对外暴露了不该暴露的接口.
那么还有其他什么办法呢?
我们想到了细分一下, AbstractBrid作为父类, 再细分出2个抽象的子类, AbstractFlyBrid, AbstractUnFlyBrid, 一个会飞的抽象鸟类, 一个不会飞的抽象鸟类, 让会飞的鸟去继承会飞的抽象鸟类, 让不会飞的鸟去继承不会飞的抽象鸟类. 如下图:
可是目前我们只是关心鸟会不会飞, 后面还会有会不会叫, 是否会下蛋等, 那么继承的关系就会越来越多, 越来越复杂.最终影响了代码的可读性和可维护性.
二. 组合相比继承有哪些优势?
我们可以用组合 + 接口 + 委托三个技术手段, 来解决上面继承存在的问题.
接口: 表示的是一种行为特性, "会飞", "会叫", "会下蛋"都是行为特性, 所以我们是不是可以定义3个接口, 会飞的接口, 会叫的接口, 会下蛋的接口, 然后让鸵鸟, 麻雀各自根据自己特有的行为去实现重写即可.
public interface Flyable {
void fly();
}
public interface Tweetable {
void tweet();
}
public interface EggLayable {
void layEgg();
}
public class Ostrich implements Tweetable, EggLayable {//鸵鸟
//... 省略其他属性和方法...
@Override
public void tweet() { //... }
@Override
public void layEgg() { //... }
}
public class Sparrow impelents Flyable, Tweetable, EggLayable {//麻雀
//... 省略其他属性和方法...
@Override
public void fly() { //... }
@Override
public void tweet() { //... }
@Override
public void layEgg() { //... }
}
以上代码到底还有没有什么问题呢?
我们知道, 接口只负责声明方法, 不定义实现. 也就是说每个会下蛋的鸟都要实现一遍layEgg()方法, 代码逻辑都一样, 这样会导致代码重复, 这个问题如何解决呢?
既然接口不负责实现, 那么我们就定义3个实现类, 分别是FlyAbility, TweetAbility, EggLayAbility. 我们可以通过组合 + 委托来消除代码重复.
/**
* 会飞接口
* @Author: Nisy
* @Date: 2023/06/14/21:19
*/
public interface Flyable {
void fly();
}
/**
* 会叫接口
* @Author: Nisy
* @Date: 2023/06/14/21:19
*/
public interface Tweetable {
void tweet();
}
/**
* 会下蛋接口
* @Author: Nisy
* @Date: 2023/06/14/21:20
*/
public interface EggLayable {
void layEgg();
}
/**
* 鸵鸟
* @Author: Nisy
* @Date: 2023/06/14/21:36
*/
public class Ostrich implements Tweetable, EggLayable {//鸵鸟
@Override
public void tweet() {
//委托(委托组合中的会叫的类来执行会叫)
tweetAbility.tweet();
}
@Override
public void layEgg() {
//委托(委托组合中会下蛋类来执行下蛋)
eggLayAbility.layEgg();
}
}
/**
* 麻雀
* @Author: Nisy
* @Date: 2023/06/14/21:36
*/
public class Sparrow impelents Flyable, Tweetable, EggLayable {//麻雀
//... 省略其他属性和方法...
@Override
public void fly() { //... }
@Override
public void tweet() { //... }
@Override
public void layEgg() { //... }
}
我们知道, 继承有3个作用:
- 表示is-a关系
- 支持多态特性
- 代码复用
而这3个作用我们可以通过其他技术手段来实现, 比如:
is-a关系, 通过组合 + 接口的has-a关系来替代
多态特性可以用接口来实现
代码复用可以用组合 + 委托来实现
组合, 接口, 委托: 委托组合中实现了的接口功能的具体实现类来实现所继承的接口的功能.
三. 如何判断该用组合还是继承?
比较简单的时候用继承, 复杂的时候尽量使用组合.
我们讲到, 继承可以实现代码复用, 但是如果A类和B类并没有具有继承关系(不是父子, 也不是兄弟关系), 强行继承会影响代码的可读性. 这时候用组合是更合理的, 比如: Crawler类和PageAnalyzer类, 都用到了Url拼接和分割功能, 这时候并不可以强行继承, 此时使用组合会更加灵活, 如下:
public class Url {
//...省略属性和方法
}
public class Crawler {
private Url url; // 组合
public Crawler() {
this.url = new Url();
}
//...
}
public class PageAnalyzer {
private Url url; // 组合
public PageAnalyzer() {
this.url = new Url();
}
//..
}
特殊场景:
有时候在一些特殊场景下, 我们必须使用继承. 如果你不能改变一个函数的入参类型, 如入参又非接口, 为了支持多态, 只能采用集成来实现, 比如FeignClient是一个外部类, 我们没有权限去修改这段代码, 我们希望重写这个类在运行时执行的encode函数, 这时候我们必须使用继承:
public class FeignClient { // Feign Client框架代码
//...省略其他代码...
public void encode(String url) { //... }
}
public void demofunction(FeignClient feignClient) {
//...
feignClient.encode(url);
//...
}
public class CustomizedFeignClient extends FeignClient {
@Override
public void encode(String url) { //...重写encode的实现...}
}
// 调用
FeignClient client = new CustomizedFeignClient();
demofunction(client);
总结:
组合并不完美, 继承也并非一无是处.