更多优质内容
请关注公众号

面向对象和设计模式(四) 使用组合代替继承——防止继承带来的类数量爆炸增长-张柏沛IT博客

正文内容

面向对象和设计模式(四) 使用组合代替继承——防止继承带来的类数量爆炸增长

栏目:其他内容 系列:面向对象与设计模式 发布时间:2023-01-01 13:55 浏览量:1223
本系列文章目录
展开/收起

为什么要“多用组合少用继承”,要回答这个问题我们需要先回顾分析继承的优缺点。

继承的优点是:代码复用,描述is-a关系,支持多态特性。

继承的缺点是:继承层次过深会降低可读性,父子类耦合不利于扩展性和维护性。

 

组合可以在保留继承的优点基础上克服继承的缺点。下面举一个例子具体说下为什么要“多用组合少用继承”,什么场景下用组合不用继承、以及组合怎么用。

 

假如我们要实现一系列的鸟类,包括麻雀、鸽子、鸵鸟、企鹅、杜鹃、蜂鸟等,需要实现他们的三个方法:叫、飞和孵蛋(tweet()、fly()和layEgg())。

要求具有这些能力的鸟必须实现他们相应能力的方法(例如麻雀是三种能力都有,因此必须实现这3个方法;鸵鸟只有 叫和孵蛋 两种能力但没有 飞 这种能力,因此必须实现这 tweet和layEgg方法,而不能实现fly()方法;蜂鸟则只有 孵蛋和飞的能力,没有叫的能力;杜鹃有 叫和飞的能力,没有孵蛋的能力,杜鹃只下蛋不孵蛋)。

 

1、使用继承的思路

实现方案1:使用AbstractBrid抽象类作为基类继承(PHP实现)

<?php

class FlyAbilityLackException extends Exception{}
class EggLayAbilityLackException extends Exception{}
class TweetAbilityLackException extends Exception{}

abstract class AbstractBird{
    abstract public function fly();
    abstract public function tweet();
    abstract public function layEgg();
}

class MaQue extends AbstractBird{   // 麻雀
    public function fly(){
        // ...具体实现
    }

    public function tweet(){
        // ...具体实现
    }

  public function layEgg(){
      // ...具体实现
  }
}

class TuoNiao extends AbstractBird{   // 鸵鸟
  public function fly(){
      throw new FlyAbilityLackException();
  }

  public function tweet(){
      // ...具体实现
  }

  public function layEgg(){
      // ...具体实现
  }
}

class FengNiao extends AbstractBird{   // 蜂鸟
  public function fly(){
      // ...具体实现
  }

  public function tweet(){
      throw new TweetAbilityLackException();
  }

  public function layEgg(){
      // ...具体实现
  }
}

这个方案在父类中声明全部的3个方法。由于鸵鸟不会飞,蜂鸟不会叫,但是由于继承了抽象类鸵鸟类和蜂鸟类不能不实现fly和tweet方法,所以只能在他们对应不会的方法上抛出异常。

看似没问题,能满足需求,但是仍有缺陷。

比如不会飞的鸟还有很多,比如企鹅。对于所有不会飞的鸟,我们都需要重写 fly() 方法,抛出异常。这会带来两个问题:

a. 徒增编码的工作量,复用性低;

b. 暴露了fly这个不该暴露的接口给外部,增加了类使用过程中被误用的概率(违反了最小知识原则)。

那为了解决上述的缺陷,用继承的思路继续优化下去可以这样做。

 

实现方案2:对AbstractBrid抽象类根据是否会飞做细分分成 AbstractFlyAbleBird 和 AbstractUnflyAbleBird类。AbstractBrid和AbstractUnflyAbleBird不实现fly()方法,AbstractFlyAbleBird实现fly()方法,并让麻雀、蜂鸟继承 AbstractFlyAbleBird ,让鸵鸟继承AbstractUnflyAbleBird。

但是这个方案有个致命问题,鸟类不仅在飞这个行为上有差异性,在叫和孵蛋行为上也有差异。如果结合 飞、叫和孵蛋这三种行为细分抽象基类,那么排列组合就会有很多个抽行类。

如果继续扩展更多行为,就会组合爆炸,扩展性几乎为0。

 

 

2、使用组合的思路

我们可以用组合、接口、委托这三个手段替代上面的继承方案。这三个手段合在一起我统称为组合(也就是上面所有提到的“组合”其实是指组合 + 接口 + 委托)。

其中,委托可以暂时简单的理解为 在一个类A的方法X中调用另一个类B的方法X,委托B来实现一部分方法X。

 

方案1:使用 接口类+组合 替代抽象+继承,但不使用委托

<?php

interface FlyAble{
    public function fly();
}

interface TweetAble{
  public function tweet();
}

interface EggLayAble{
  public function layEgg();
}

class MaQue implements FlyAble,TweetAble,EggLayAble{   // 麻雀
    public function fly(){
        // ...具体实现
    }

    public function tweet(){
        // ...具体实现
    }

  public function layEgg(){
      // ...具体实现
  }
}

class TuoNiao implements TweetAble,EggLayAble{   // 鸵鸟
  public function tweet(){
      // ...具体实现
  }

  public function layEgg(){
      // ...具体实现
  }
}

class FengNiao implements FlyAble,EggLayAble{   // 蜂鸟
  public function fly(){
      // ...具体实现
  }

  public function layEgg(){
      // ...具体实现
  }
}

这个方案的缺陷在于,如果fly()方法具有可复用的逻辑,那么所有会飞的鸟类的fly方法中都要重复实现fly中可复用的逻辑。

因此该方案虽然解决了继承的耦合和继承层级深的缺点,却遗失了继承的可复用性。

接下来我们看下一个方案。

 

方案2:使用 接口类+组合+委托 替代抽象+继承

具体来说,我们可以将 fly()、tweet()和layEgg() 中可复用的逻辑提出来放到3个类FlyAbility、TweetAbility和EggLayAbility中,在具体的鸟类的fly方法里可以调用Ability类的fly方法满足行为的公共逻辑,再实现该具体鸟类中fly的独有逻辑。

<?php

interface FlyAble{
    public function fly();
}

interface TweetAble{
  public function tweet();
}

interface EggLayAble{
  public function layEgg();
}

class FlyAbility{   // 委托类
  public function fly(){
    // ...飞的公共逻辑
  }
}

class TweetAbility{
  public function tweet(){
    // ...叫的公共逻辑
  }
}

class EggLayAbility{
  public function layEgg(){
    // ...孵蛋的公共逻辑
  }
}

class MaQue implements FlyAble,TweetAble,EggLayAble{   // 麻雀
  protected $flyAbility;
  protected $tweetAbility;
  protected $eggLayAbility;

  public function __construct()
  {
    $this->tweetAbility = new TweetAbility();
    $this->flyAbility = new FlyAbility();
    $this->eggLayAbility = new EggLayAbility();
  }

  public function fly(){
      $this->flyAbility->fly();   // 委托
      // ...具体实现
  }

  public function tweet(){
    $this->tweetAbility->tweet();
      // ...具体实现
  }

  public function layEgg(){
    $this->eggLayAbility->layEgg();
      // ...具体实现
  }
}

class TuoNiao implements TweetAble,EggLayAble{   // 鸵鸟
  protected $tweetAbility;
  protected $eggLayAbility;
  public function __construct()
    {
      $this->tweetAbility = new TweetAbility();
      $this->eggLayAbility = new EggLayAbility();
    }
  public function tweet(){
    $this->tweetAbility->tweet();
      // ...具体实现
  }

  public function layEgg(){
    $this->eggLayAbility->layEgg();
      // ...具体实现
  }
}

class FengNiao implements FlyAble,EggLayAble{   // 蜂鸟
  protected $flyAbility;
  protected $eggLayAbility;

  public function __construct()
  {
    $this->flyAbility = new FlyAbility();
    $this->eggLayAbility = new EggLayAbility();
  }

  public function fly(){
      $this->flyAbility->fly();
      // ...具体实现
  }

  public function layEgg(){
    $this->eggLayAbility->layEgg();
      // ...具体实现
  }
}

 

该方案已经能够彻底弥补继承的缺点,有保持继承的优点。

但是不是说它就已经没有缺陷了呢,假如需要扩展更多的行为(例如我要再扩展一个“迁徙”行为),就意味着我要定义更多的Ability类和Able接口。并且在实现类中,我们还要在构造函数中实例化更多的Ability类。而类和接口的增多意味着代码的复杂程度和维护成本的提高。

此时我们应该考虑Ability类和Able接口类的拆分粒度。例如我们是不是可以将一些无可分割的方法放到一个Able接口和Ability类中,而不是为每个方法都定义一个Able接口和Ability类。例如read、write和execute方法,方法read和方法write放到一个 ReadWriteAble 接口和ReadWriteAbility类,execute 放到一个 ExecuteAble 接口和ExecuteAbility类。

至于具体的拆分粒度,取决于具体的业务场景。

在这个鸟类的案例中,我们的最佳方案其实是将3个方法整合到一个Ability类,而接口上需要拆分为3个Able接口类。

 

方案3:最终方案

<?php

interface FlyAble{
    public function fly();
}

interface TweetAble{
  public function tweet();
}

interface EggLayAble{
  public function layEgg();
}

class BirdAbility{   // 委托类
  public function fly(){
    // ...飞的公共逻辑
  }

  public function tweet(){
    // ...叫的公共逻辑
  }

  public function layEgg(){
    // ...孵蛋的公共逻辑
  }
}

class Bird{   // 子类继承自Bird类,将实例化Ability类的逻辑收归到Bird类中,进一步提升复用性
  protected $birdAbility;
  public function __construct()
  {
    $this->birdAbility = new BirdAbility();
  }

  // ...鸟类的其他方法
}

class MaQue extends Bird implements FlyAble,TweetAble,EggLayAble{   // 麻雀
  public function fly(){
      $this->birdAbility->fly();   // 委托
      // ...具体实现
  }

  public function tweet(){
    $this->birdAbility->tweet();
      // ...具体实现
  }

  public function layEgg(){
    $this->birdAbility->layEgg();
      // ...具体实现
  }
}

class TuoNiao extends Bird implements TweetAble,EggLayAble{   // 鸵鸟
  public function tweet(){
    $this->birdAbility->tweet();
      // ...具体实现
  }

  public function layEgg(){
    $this->birdAbility->layEgg();
      // ...具体实现
  }
}

class FengNiao extends Bird implements FlyAble,EggLayAble{   // 蜂鸟
  public function fly(){
      $this->birdAbility->fly();
      // ...具体实现
  }

  public function layEgg(){
    $this->birdAbility->layEgg();
      // ...具体实现
  }
}

 

处了上面的做法之外,对于PHP而言,我们可以用trait特性轻松的实现组合,具体做法是在新建一个BirdAblityTrait类,在trait类中定义 flyAblity() 方法实现飞的公共逻辑,然后再在具体的某个鸟类中调用flyAblity()方法,这样就可以无需定义BirdAblity类,具体的鸟类也无需依赖$this->birdAbility对象。如下所示:

<?php

interface FlyAble{
    public function fly();
}

interface TweetAble{
  public function tweet();
}

interface EggLayAble{
  public function layEgg();
}

trait BirdAbilityTrait{
  public function flyAbility(){
    // ...飞的公共逻辑
  }

  public function tweetAbility(){
    // ...叫的公共逻辑
  }

  public function layEggAbility(){
    // ...孵蛋的公共逻辑
  }
}

class Bird{
  // ...鸟类的其他方法
}

class MaQue extends Bird implements FlyAble,TweetAble,EggLayAble{   // 麻雀
  use BirdAbilityTrait;   // 引入 trait 类
  public function fly(){
      $this->flyAbility();   // 调用trait类的方法
      // ...具体实现
  }

  public function tweet(){
    $this->tweetAbility();  // 调用trait类的方法
      // ...具体实现
  }

  public function layEgg(){
    $this->layEggAbility(); // 调用trait类的方法
      // ...具体实现
  }
}

class TuoNiao extends Bird implements TweetAble,EggLayAble{   // 鸵鸟
  use BirdAbilityTrait;
  public function tweet(){
    $this->flyAbility();   // 调用trait类的方法
      // ...具体实现
  }

  public function layEgg(){
    $this->layEggAbility(); // 调用trait类的方法
      // ...具体实现
  }
}

class FengNiao extends Bird implements FlyAble,EggLayAble{   // 蜂鸟
  use BirdAbilityTrait;
  public function fly(){
    $this->flyAbility();   // 调用trait类的方法
    // ...具体实现
  }

  public function layEgg(){
    $this->layEggAbility(); // 调用trait类的方法
      // ...具体实现
  }
}

 

综合上面的例子,我们得出结论,如果多个类具有is-a关系但是这些类之间的方法集合不尽相同,就需要尽量用到组合代替继承。

最后虽然我们说鼓励多用组合少用继承,但是具体情况要看具体业务场景,我们不能说为了设计原则而用设计原则,而是为了实现可读性、可复用性、可扩展性和可维护性,不忘初心。在不复杂的业务场景中,继承足够满足我们的需求且没有明显的扩展性缺陷,我们就可以不用组合(毕竟组合意味着要做更细粒度的类的拆分,类和接口的增多会增加代码的复杂程度和维护成本)。

类之间存在is-a关系的基础上,如果类之间的继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之则尽量使用组合来替代继承。

如果类之间不具备is-a关系,只具备has-a关系,则只能使用组合。

如果子类A不能够实现父类B的所有抽象方法(例如B中有些方法是A不需要的),那么建议A和B之间使用组合而不用继承,并且让A和B继承自一个更公共的父类C。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > 面向对象和设计模式(四) 使用组合代替继承——防止继承带来的类数量爆炸增长

热门推荐
推荐新闻