继承和组合,我该怎么选择?

在面向对象编程中,有一条非常经典的设计原则,那就是:**组合优于继承,多用组合少用继承。为什么不推荐使用继承?组合相比继承有哪些优势?

为什么不推荐使用继承

继承是面向对象的四大特征之一,表示表示类之间的is-a关系,可以解决代码复用的问题,但是继承带来了一个最大的坏处破坏封装,相比之下,组合也是实现类复用的重要方式,而采用组合方式来实现类复用则能提供很好的封装。虽然继承有很多的作用,但继承层次过深、过复杂,也会影响代码的可维护性。

我们看下面这个例子:

我们定义一个 鸟类的抽象类AbstractBird,我们都知道,大部分鸟都会飞,那我们可不可以在AbstractBird 抽象类中,定义一个 fly() 方法? 答案是否定的?尽管我们都知道大部分的鸟都是会飞的,但是也有特例,比如鸵鸟就不会飞,鸵鸟集成具有fly() 的方法的父类,那鸵鸟的具有"飞"这样的行为,这显然不符合对现实世界中事物的认识。那么如果我们在鸵鸟这个子类中重写 fly() 方法,让它抛出 UnSupportedMethodException 异常不就可以了吗?

public abstract  class AbstractBird {
    abstract void fly();
}
/**
 * 鸵鸟
 */
public class Ostrich extends AbstractBird{
    @Override
    void fly() {
        throw  new UnsupportedMethodException("我不会飞!!!");
    }
}

这种做法确实可以解决问题,但是不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟 ,我们都需要重写fly() 方法,抛出异常。这样的设计,一方面,新增了变量的工作量 ;另一方面,也违背了最小知识原则,暴露了不该暴露的接口给外部,增加了类使用过程中被误用的概率。

那我可以通过 AbstractBird 类派生出两个更加细分的抽象类;会飞的鸟类 AbstractFlyableBird 和 不会飞的鸟 AbstractUnFlyableBird ,让麻雀、乌鸦这些会飞的鸟都继承AbstractFlyableBird ,让鸵鸟这些不会飞的鸟直接继承 AbstractUnFlyableBird 类,不就可以了,继承关系如下:

在这里插入图片描述
从上图中,继承关系变成了三层。如果上面再继续加大难度,我们再关注鸟会不会叫,又如何设计之间的层次关系呢?

是否会飞?是否会叫?两个行为搭配起来会产生四种情况:会飞会叫、不会飞会叫、会飞不会叫、不会飞不会叫。如果我们继续沿用刚才的设计思路,那就需要再定义四个抽象类(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)
在这里插入图片描述
如果还要考虑会不会下蛋,则会更加复杂,类的继承层次会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系,一方面,会导导致代码的可读性差。因为我们要搞清某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码…一直追溯到最顶层的父类的代码、
另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。

组合相比继承有哪些优势?

实际上,我们可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。

接口表示具有某种行为特性。针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这些行为特性,我们可以类似地定义 Tweetable 接口、EggLayable 接口。我们将这个设计思路翻译成 Java 代码的话,就是下面这个样子:


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()方法,并且实现的逻辑都是一样的,这就会导致代码的重复的问题,那么该怎么解决呢?

我们可以针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托技术来消除代码重复。具体的代码实现如下所示:


public interface Flyable {
  void fly()}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鸵鸟
  private TweetAbility tweetAbility = new TweetAbility(); //组合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
  //... 省略其他属性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委托
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委托
  }
}

到底该用继承?还是该用组合呢? 继承是对已有的类做一番改造,一次获得有一个特殊的版本。简而言之,就是将一个较为抽象的类改造成能适用于某些特定需求的类。多态特性我们可以利用接口来实现;代码复用我们可以通过组合和委托来实现。

如何判断该用组合还是继承?

尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。从上面的例子来看,继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。所以,在实际的项目开发中,我们还是要根据具体的情况,来具体选择该用继承还是组合。

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

尽管有些人说,要杜绝继承,100% 用组合代替继承,但是我的观点没那么极端!之所以“多用组合少用继承”这个口号喊得这么响,只是因为,长期以来,我们过度使用继承。还是那句话,组合并不完美,继承也不是一无是处。只要我们控制好它们的副作用、发挥它们各自的优势,在不同的场合下,恰当地选择使用继承还是组合,这才是我们所追求的境界

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半夏_2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值