组合优于继承:什么情况下可以使用继承?

C++设计模式专栏:http://t.csdnimg.cn/8Ulj3

目录

1.引言

2.为什么不推荐使用继承

3.相比继承,组合有哪些优势

4.如何决定是使用组合还是使用继承


1.引言

        面向对象编程中有一条经典的设计原则:组合优于继承,也常被描述为多用组合,少用继承。为什么不推荐使用继承?相比继承,组合有哪些优势?如何决定是使用组合还是使用继承?本节围绕这3个问题详细讲解这条设计原则。

2.为什么不推荐使用继承

        继承是面向对象编程的四大特性之一,用来表示类之间的is-a关系,可以解决代码复用问题。虽然继承有诸多作用,但继承层次过深、过复杂,会影响代码的可维护性。对于是否应在项目中使用继承,目前存在很多争议。很多人认为继承是一种反模式,应该尽量少用,甚至不用。为什么会有这样的争议呢?我们通过一个例子解释一下。

        假设我们要设计一个关于鸟的类。我们将“鸟”这样一个抽象的事物概念定义为一个抽象类 AbstractBird。所有细分的鸟,如麻雀、鸽子和乌鸦等,都继承这个抽象类。我们知道,大部分鸟都会飞,那么可不可以在AbstractBird抽象类中定义一个fly()方法呢?答案是否定的。尽管大部分鸟都会飞,但也有特例,如鸵鸟就不会飞。鸵鸟类继承具有fly()方法的父类,那么鸵鸟就具有了“飞”这样的行为,这显然不符合我们对现实世界中事物的认识。当然,读者可能会说,在鸵鸟这个子类中重写(overide)fly()方法,让它抛出UnSupportedMethodException异常不就可以了吗?具体的代码实现如下。

public class AbstractBird{
    //...省略其他属性和方法...
    public void fly(){...}
}
public class 0strich extends AbstractBird {
     //轮鸟类//.省略其他属性和方法.
    public void fly(){
        throw new unsupportedMethodException("I can't fy.");
    }
}

        虽然这种设计思路可以解决问题,但不够优雅,因为除鸵鸟以外,不会飞的鸟还有一些,如企鹅,对于所有不会飞的鸟,我们都需要重写fly()方法,并抛出异常。这样的设计,一方面,徒增编码的工作量; 另一方面,违背最少知识原则(迪米特法则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。

        读者可能又会说,可以通过 AbstractBird类派生出两个细分的抽象类:AbstractFlyableBird(会飞的鸟类)和AbstractUnFlyableBird(不会飞的鸟类),让麻雀、乌鸦这些会飞的鸟对应的类都继承AbstractFlyableBird类,让鸵鸟、企鹅这些不会飞的鸟对应的类都继承AbstractUnFlyableBird类,如下图所示。是不是就可以解决问题了呢?

        从上图中,我们可以看出,继承关系变成了3层。从整体上来讲,目前的继承关系还比较简单,层次比较浅,也算是一种可以接受的设计思路。我们继续添加需求。在上文提到的场景中,我们只关注“鸟会不会飞”,但如果我们还要关注“鸟会不会叫”,那么,这个时候,又该如何设计类之间的继承关系呢?

        是否会飞和是否会叫可以产生4种组合:会飞会叫、不会飞但会叫、会飞但不会叫、不会飞不会叫。如果沿用上面的设计思路,那么需要再定义4个抽象类:AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird 和 AbstractUnFlyableUnTweetableBind。此处的继承关系如下图所示。

        如果我们还需要考虑“是否会下蛋”,那么组合数量会呈指数式增长。也就是说,类的继承层次会越来越深,继承关系会越来越复杂。这种层次很深、很复杂的继承关系会导致代码的可读性变差,因为我们要弄清楚某个类包含哪些方法、属性,就必须阅读父类的代码、父类的父类的代码……一直追溯到顶层父类。另外,这破坏了类的封装特性,因为将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,二者高度耦合,一旦父类的代码被修改,那么会影响所有的子类。

        总之,继承最大的问题就在于:继承层次过深、继承关系过于复杂,会影响代码的可读性和可维护性。这也是我们不推荐使用继承的原因。对于本例中继承存在的问题,我们应该如何解决呢?读者可以在下文中得到答案。

3.相比继承,组合有哪些优势

        实际上,我们可以通过组合(composition)、接口和委托(delegation)3种技术手段共同解决上面继承存在的问题。

        在介绍接口时,我们说过,接口表示具有某种行为特性。针对“会飞”这样一个我们可以定文一个接口Flyable,只让会飞的鸟去实现这个接口。对于会叫、会下蛋这两个行为特性,可以类似地分别定义Tweetable接口、EggLayable接口。我们将此设计思路翻译成Java 代码如下所示。

public interface Flyable {
    void fly();
}
public interface Tweetable {
    void tweet();
}
public interface EggLayable {
    void layEgg();
}
public class 0strich 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个接口再定义3个实现类: 实现了fly()方法的FlyAbility类、实现了twee()方法的TweetAbility类和实现了layEgg()方法的EggLayAbility类。然后,我们通过组合和委托技法消除代码的重复问题。具体的代码实现如下。

public interface Flyable {
    void fly();
}
public class FlyAbility implements Flyable{
    @Override
    public void fly(){... }
}

//省略Tweetable接口、Tweetability类、
//EggLayable接口和EggLayAbility类的代码实现
public class 0strich implements Tweetable, Egglayable {  
    private TweetAbility tweetability = new Tweetabil1ty();//轮鸟类
    private EggLayabiliey eggLaynbility = new EggLayAbi1ity();//组合
    //1省略其他属性和方法
    @Override
    public void tweet(){
        tweetAbility.tweet();//委托
    };
    @Override
    public void layEgg(){
        eggLayAbility.layEgg();//委托
    }
}

        我们知道,继承主要有3个作用:表示is-a关系、支持多态特性和代码复用。而这3个作用都可以通过其他技术手段来达成。例如,is-a关系可以通过组合和接口的has-a关系替代; 多态特性可以利用接口实现;代码复用可以通过组合和委托实现。从理论上来讲,组合、接口和委托3种技术手段完全可以替代继承。因此,在项目中,我们可以不用或少用继承关系,特别是一些复杂的继承关系。

4.如何决定是使用组合还是使用继承

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

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

        一些特殊的场景要求必须使用继承。如果我们不能改变一个函数的入参类型,而入参又非接口,那么,为了支持多态,只能采用继承来实现。例如下面这段代码,其中的 FeignClient类是一个外部类,我们没有权限修改这部分代码,但是,我们希望能够重写这个类在运行时执行的encode()函数。这个时候,我们只能采用继承来实现。

public class FeignClient{
     //Feign client框架代码11...省略其他代码...
    public void encode(string url){...}
}
public class CustomizedFeignclient extends FeignClient {
    @Override
    public void encode(string url){
        //...省略重写encode()的实现代码..
    }
}
public void demofunction(FeignClient feignClient){
    //...省略部分代码.
    feignClient.encode(url);
    //省略部分代码...
}

//调用
FeignClient client=new CustomizedFeignClient();
demofunction(client);

        之所以推荐“多用组合,少用继承”,是因为长期以来,很多程序员过度使用继承,还那句话,组合并非完美,继承也不是一无是处。控制好它们的副作用,发挥它们各自的优势在不同的场合下,恰当地选择使用继承或组合,这才是我们应该追求的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/574576.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript原理篇——深入理解作用域、作用域链、闭包、this指向

执行上下文描述了代码执行时的环境,包括变量对象、作用域链和 this 值;而作用域则决定了变量和函数的可访问性范围,分为全局作用域和局部作用域。 变量对象用于存储变量和函数声明:是与执行上下文相关联的数据结构,用于…

USB设备的音频类UAC

一、UAC简介 UAC(USB Audio Class)是USB设备的音频类,它定义了USB音频设备与主机计算机通信的方式。UAC标准是USB规范的一部分,并受到各种操作系统(包括Windows、macOS和Linux)的支持。 UAC是基于libusb,实…

代码随想录算法训练营第五十一天| 309.最佳买卖股票时机含冷冻期,714.买卖股票的最佳时机含手续费,总结

题目与题解 参考资料:买卖股票总结 309.最佳买卖股票时机含冷冻期 题目链接:309.最佳买卖股票时机含冷冻期 代码随想录题解:309.最佳买卖股票时机含冷冻期 视频讲解:动态规划来决定最佳时机,这次有冷冻期!|…

【自然语言处理】InstructGPT、GPT-4 概述

InstructGPT官方论文地址:https://arxiv.org/pdf/2203.02155.pdf GPT-4 Technical Report:https://arxiv.org/pdf/2303.08774.pdf GPT-4:GPT-4 目录 1 InstructGPT 2 GPT-4 1 InstructGPT 在了解ChatGPT之前,我们先看看Instr…

k8s pod 无法启动一直ContainerCreating

情况如下,更新 pod 时,一直在ContainerCreating 查看详细信息如下 Failed to create pod sandbox: rpc error: code Unknown desc [failed to set up sandbox container “334d991a478b9640c66c67b46305122d7f0eefc98b2b4e671301f1981d9b9bc6” networ…

Jsoncpp搭建交叉编译环境(移植到arm)

1. 官网下载源码 github地址:GitHub - open-source-parsers/jsoncpp at update 2. 交叉编译环境 当前平台/开发平台-编译环境: [rootlocalroot ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) [rootlocalroot ~]# uname -a Lin…

Django框架之Django安装与使用

一、Django框架下载 首先我们需要先确定好自己电脑上的python解释器环境,否则会导致后面项目所需要的库安装不了以及项目无法运行的问题。 要下载Django并开始使用它,你可以按照以下步骤进行: 1、安装Python 首先,确保你的计算…

C/C++开发,opencv-ml库学习,支持向量机(SVM)应用

目录 一、OpenCV支持向量机(SVM)模块 1.1 openCV的机器学习库 1.2 SVM(支持向量机)模块 1.3 支持向量机(SVM)应用步骤 二、支持向量机(SVM)应用示例 2.1 训练及验证数据获取 2…

报错:OpenGL.error.NullFunctionError: Attempt to call an undefined function”

文件我已经上传 CSDN默认就是收费的 我修改不了 免费链接在文中 请寻找 OpenGL.error.NullFunctionError: Attempt to call an undefined function” 环境陈述: windows11 AMD-R9 python版本3.9.9 背景: 通过pip安装pip install PyOpenGL安装PyOpenGL模块后 运行出现的问题…

NLP Step by Step -- How to use pipeline

正如我们在摸鱼有一手:NLP step by step -- 了解Transformer中看到的那样,Transformers模型通常非常大。对于数以百万计到数千万计数十亿的参数,训练和部署这些模型是一项复杂的任务。此外,由于几乎每天都在发布新模型&#xff0c…

数据挖掘实验一

一、实验环境及背景 使用软件: Anaconda3 Jupyter Notebook 实验内容: 1.使用Tushare或者其他手段获取任意两支股票近三个月的交易数据。做出收盘价的变动图像。2.使用Pandas_datareader获取世界银行数据库中美国(USA)、瑞典&…

Windows电脑中护眼(夜间)模式的开启异常

我的电脑是联想小新16pro,Windows11版本。之前一直可以正常使用夜间模式,但是经过一次电脑的版本更新之后,我重启电脑发现我的夜间模式不能使用了。明明显示开启状态,但是却不能使用,电脑还是无法显示夜间模式。 询问…

基于Spring Boot的考研资讯平台设计与实现

基于Spring Boot的考研资讯平台设计与实现 开发语言:Java框架:springbootJDK版本:JDK1.8数据库工具:Navicat11开发软件:eclipse/myeclipse/idea 系统部分展示 系统功能界面图,在系统首页可以查看首页、考…

【Qt QML】TabBar的用法

Qt Quick中的TabBar提供了一个基于选项卡的导航模型。TabBar由TabButton控件填充,并且可以与任何提供currentIndex属性的布局或容器控件一起使用,例如StackLayout或SwipeView。 import QtQuick import QtQuick.Controls import QtQuick.LayoutsWindow …

FPGA实现AXI4总线的读写_如何写axi4逻辑

FPGA实现AXI4总线的读写_如何写axi4逻辑 一、AXI4 接口描述 通道信号源信号描述全局信号aclk主机全局时钟aresetn主机全局复位,低有效写通道地址与控制信号通道M_AXI_WR_awid[3:0]主机写地址ID,用来标志一组写信号M_AXI_WR_awaddr[31:0]主机写地址&…

贪吃蛇身子改进加贪吃蛇向右移动

1. 蛇移动的思想: 其实就是删除头节点 ,增加尾节点;一句代码搞定 struct Snake *p; p head; head head -> next; free(p) 防止造成多的空间节点 2.增加尾节点代码思想: 2.1 .开辟new 节点的空间 struct Snake *new (stru…

每日OJ题_DFS回溯剪枝①_力扣46. 全排列(回溯算法简介)

目录 回溯算法简介 力扣46. 全排列 解析代码 回溯算法简介 回溯算法是一种经典的递归算法,通常⽤于解决组合问题、排列问题和搜索问题等。 回溯算法的基本思想:从一个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态无…

Quarto Dashboards 教程 3:Dashboard Data Display

「写在前面」 学习一个软件最好的方法就是啃它的官方文档。本着自己学习、分享他人的态度,分享官方文档的中文教程。软件可能随时更新,建议配合官方文档一起阅读。推荐先按顺序阅读往期内容: 1.quarto 教程 1:Hello, Quarto 2.qu…

耐酸碱腐蚀PFA冷凝回流装置进口透明聚四氟材质PFA梨形漏斗特氟龙圆底烧瓶

PFA分液漏斗:也叫特氟龙分液漏斗、特氟龙梨型分液漏斗。 规格参考:125ml、250ml、500ml、1000ml 其主要特性有: 1.内壁对溶剂无粘贴性和吸附,可完全排空,分界面清晰可见; 2.密封性好,可防止…

excel文件导入dbeaver中文乱码

1.将excel文件进行另存为,保存类型选择【CSV】 2.选择【工具】–>【web选项】–> 【编码】–> 【简体中文(GB18030)】 3.在DBeaver进行数据导入 直接导入应该就可以,如果不行的话按下面处理。 选择【导入数据——选择cs…
最新文章