WEB视频自适应 循环 个人收款码 mongoose phpmyadmin vue循环数组 vue入门 管理后台ui jquery绑定change事件 kafka启动命令 安装python教程 python编译环境 python中import用法 java类 java连数据库 java替换字符串 java数组 linux安装 python源码下载 键盘宏软件 beatedit oxm 快点蛆虫成就单刷 pr转场特效下载 vfloppy 在线手册 fireworks8序列号 微信骰子表情包 bz2解压命令 开源即时通讯软件 fireworks qq悄悄话怎么知道对方是谁 EarthView php验证码 任务管理软件 无线中继是什么意思 spring拦截器 ps画笔工具变成十字 cad合并成块 俄罗斯方块java
当前位置: 首页 > 学习教程  > 编程语言

【设计模式】面向对象(四):多用组合少用继承

2021/4/19 23:30:55 文章标签:

1.为什么不推荐使用继承? 继承是面向对象的四大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。 虽然继承有诸多作用,但也会带来许多问题,在如下情况,我们应该尽量少用,甚至不用…

1.为什么不推荐使用继承?

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

  • 虽然继承有诸多作用,但也会带来许多问题,在如下情况,我们应该尽量少用,甚至不用继承

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

    • 若单纯为了复用而抽象出父类,会违背is-a关系,影响可读性

示例一

需求1

假设我们要设计一个关于鸟的类。我们将“鸟类”这样一个抽象的事物概念,定义为一个抽象类 AbstractBird。所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类我们知道,大部分鸟都会飞,那我们可不可以在 AbstractBird 抽象类中,定义一个 fly() 方 法呢?

  • 答案是否定的。尽管大部分鸟都会飞,但也有特例,比如鸵鸟就不会飞
  • 鸵鸟继承具 有 fly() 方法的父类,那鸵鸟就具有“飞”这样的行为,这显然不符合我们对现实世界中事物的认识

方案1 & 问题

  • 方案:在鸵鸟这个子类中重写(override)fly() 方法,让它抛出 UnSupportedMethodException 异常
public class AbstractBird {  
    //... 省略其他属性和方法... 
    public void fly() { //... 
    }
}
public class Ostrich extends AbstractBird { // 鸵鸟 
    //... 省略其他属性和方法... 
    public void fly() {    
        throw new UnSupportedMethodException("I can't fly.'");
    } 
}
  • 问题
    • 这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多, 比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 fly() 方法,抛出异常
    • 一方面,徒增了编码的工作量
    • 另一方面,也违背了我们小知识原则 (Least Knowledge Principle,也叫少知识原则或者迪米特法则),暴露不该暴露的接 口给外部,增加了类使用过程中被误用的概率

方案2 & 问题

  • 方案
    • 那我们再通过 AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird
    • 让麻雀、乌鸦这些会飞的 鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类,具体的继承关系如下图所示:

在这里插入图片描述

  • 问题:从图中我们可以看出,继承关系变成了三层。不过,整体上来讲,目前的继承关系还比较简 单,层次比较浅,也算是一种可以接受的设计思路

需求2

在刚刚这个场景中,我们只关注“鸟会不会飞”,但如果我们还关注“鸟会不会叫”,那这个时候,我们又 该如何设计类之间的继承关系呢?

方案3 & 问题

  • 方案
    • 是否会飞?是否会叫?两个行为搭配起来会产生四种情况:会飞会叫、不会飞会叫、会飞不 会叫、不会飞不会叫。
    • 如果我们继续沿用刚才的设计思路,那就需要再定义四个抽象类 (AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、 AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)。

在这里插入图片描述

  • 问题
    • 如果我们还需要考虑“是否会下蛋”这样一个行为,那估计就要组合爆炸了。类的继承层次 会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系。
    • 一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到顶层父类的代码。
    • 另一方面,这也破坏了类的封装 特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦 父类代码修改,就会影响所有子类的逻辑。

示例二

需求

Crawler 类和 PageAnalyzer 类,它们都用到了 URL 拼接和分割的功能,现在将url操作抽象出来

方案1 & 问题

方案:Crawler 和 PageAnalyzer 类继承抽象出来的URL类

public class Url {  
    //... 省略属性和方法
}
public class Crawler extends Url {...}

public class PageAnalyzer extends Url{...}

问题:可读性差,C类P类与U类并不具备is-a关系

总之,继承大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和 可维护性。这也是为什么我们不推荐使用继承。

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

  • 继承主要有三个作用:表示 is-a 关系,支持多态特性,代码复用。而这三个作用都可以通 过组合、接口、委托三个技术手段来达成
    • is-a 关系:通过组合和接口的 has-a 关系 来替代
    • 多态特性:利用接口来实现
    • 代码复用:通过组合和委托来实现
  • 除此之外,利用组合还能解决层次过深、过复杂 的继承关系影响代码可维护性的问题。

示例一

需求2

在刚刚这个场景中,我们只关注“鸟会不会飞”,但如果我们还关注“鸟会不会叫”,那这个时候,我们又 该如何设计类之间的继承关系呢?

方案4 & 问题

  • 方案
    • 接口表示具有某种行为特性。
    • 针对“会飞”这样一个行为特 性,我们可以定义一个 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 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(); // 委托
    } 
}

示例二

需求

Crawler 类和 PageAnalyzer 类,它们都用到了 URL 拼接和分割的功能,现在将url操作抽象出来

最终方案

方案:将Url类组合到C P

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(); 
    }  
    //.. 
}

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

尽管我们鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。在实际的 项目开发中,我们还是要根据具体的情况,来选择该用继承还是组合。

  • 如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以大胆地使用继承。
  • 反之,我们就尽量使用组 合来替代继承。
  • 除此之外,还有一些设计模式、特殊的应用场景,会固定使用继承或者组合。
    • 组合:装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)
    • 继承:而模板模式(template pattern)

本文链接: http://www.dtmao.cc/news_show_942358.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?