设计模式、面向对象六:继承与组合

继承

继承是面向对象的四大特性之一,用于表示类之间的is-a关系,可以解决代码复用的问题,支持多态。

继承的缺点

继承层次过深、过复杂化,会影响到代码的可维护性。


举例:

假设我们要设计一个关于鸟的类。

  • 将鸟类这样的一个抽象的事物概念,定义为抽象类AbstractBird
  • 所有更细分的鸟,比如麻雀、鸽子、等都继承这个抽象类

问题:大部分鸟会飞,我们可以在AbstractBird抽象类中,定义个fly()方法吗?
答案:否定
解答:因为大部分鸟会飞,但是有特例,比如鸵鸟不会飞。 鸵鸟继承具有fly()方法的父类,那鸵鸟就具有飞的行为,这不符合预期。
解决方法

  1. 方法一
    我们可以在鸵鸟的子类中重写fly()方法,让它抛出异常

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

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

  2. 方法二
    可以通过AbstractBird类派生出两个更细分的抽象类:会飞的鸟类AbstractFlyableBird和不会飞的鸟类AbstractUnflyableBird,让 不会飞的鸟类和会飞的鸟继承各自属于的类
    在这里插入图片描述
    方法评测:继承关系变成三层。整体上来讲,目前的继承关系还比较简单,可以接受。但是继续加难度,比如加个鸟会不会叫,这个时候 沿用刚才的思路,继续定义四个抽象类。这种方式会使类的层次越来越深。

    • 一方面会导致代码的可读性变差。因为要搞清楚某个类的方法属性,需要追溯到最顶层的父类
    • 另一方面,破坏了类的封装特性,将父类实现的更多细节暴露给子类,两者高度耦合,一旦父类代码修改,会影响所有子类

组合

利用组合解决继承的问题

我们可以利用 组合、接口、委托 这个三个技术手段解决上面继承存在的问题

方法
接口表示具有某些行为特性。所以可以定义多个行为特性定义成多个接口供类实现,而继承因为只能支持单继承所以无法用这种方法

  • 针对会飞这个行为特性,我们可以定义一个Flyabe接口,只让会飞的鸟实现这个接口
  • 针对会叫、会下蛋的这些行为特性,可以定义Tweetable接口、EggLayable接口

代码如下


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 Flayable, 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(); // 委托
  }
}

继承的副作用,以及如何用组合解决

继承可以实现代码复用,我们可以利用继承,把相同属性和方法,抽取出来,定义到父类中。达到代码复用的目的。
但是有时候,从业务的含义上,A类和B类不一定具有继承关系。 比如Crawler和PageAnalyzer类,他们都用到了URL拼接和分隔的功能,但不具有继承关系。仅仅是为了代码复用,声明的抽象出一个父类来,会影响代码的可读性,如果不熟悉背后设计思路的同事,发现这两个类继承同一个父类,而父类仅仅是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();
  }
  //..
}

继承的特殊场景

有些特殊的场景, 你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承实现。
比如下面代码:


public class FeignClient { // feighn 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);

其中FeignClient是一个外部类,我们没有权限去修改这部分代码,但是我们希望重写这个类在运行时执行的encode()函数,这时候,我们只能采用继承

总结

继承主要有三个作用:标识is-a关系,支持多态特性,代码复用。
这个三个作用都可以用其他技术手段来达成。

  • 比如is-a关系,我们可以通过组合和接口的has-a关系来替代
  • 多态特性我们可以用接口来实现
  • 代码复用我们可以通过组合和委托来实现。

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值