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

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{//轮鸟类//...省略其他属性和方法.@Overridepublic void tweet(){ ...}@Overridepublic void layEgg(){... }
}
public class Sparrow impelents Flyable, Tweetable, EggLayable {//麻雀类//...省略其他属性和方法...@Overridepublic void fly(){... }@Overridepublic void tweet(){...}@Overridepublic void layEgg(){...}
}

        不过,我们知道,接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现遍layEgg()方法,并且实现逻辑是一样的,这就会导致代码重复的问题。对于这个问题,我们可以以针对3个接口再定义3个实现类: 实现了fly()方法的FlyAbility类、实现了twee()方法的TweetAbility类和实现了layEgg()方法的EggLayAbility类。然后,我们通过组合和委托技法消除代码的重复问题。具体的代码实现如下。

public interface Flyable {void fly();
}
public class FlyAbility implements Flyable{@Overridepublic void fly(){... }
}//省略Tweetable接口、Tweetability类、
//EggLayable接口和EggLayAbility类的代码实现
public class 0strich implements Tweetable, Egglayable {  private TweetAbility tweetability = new Tweetabil1ty();//轮鸟类private EggLayabiliey eggLaynbility = new EggLayAbi1ity();//组合//1省略其他属性和方法@Overridepublic void tweet(){tweetAbility.tweet();//委托};@Overridepublic 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 {@Overridepublic void encode(string url){//...省略重写encode()的实现代码..}
}
public void demofunction(FeignClient feignClient){//...省略部分代码.feignClient.encode(url);//省略部分代码...
}//调用
FeignClient client=new CustomizedFeignClient();
demofunction(client);

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

链游:未来游戏发展的新风向

链游,即区块链游戏的一种,是一种将区块链技术与游戏玩法相结合的创新型游戏。它利用区块链技术的特性,如去中心化、可追溯性和安全性,为玩家提供了一种全新的游戏体验。链游通常采用智能合约来实现游戏的规则和交易系统&#xff0…

计算机网络和因特网

Internet: 主机/端系统(end System / host): 硬件 操作系统 网络应用程序 通信链路: 光纤、网络电缆、无线电、卫星 传输效率:带宽(bps) 分组交换设备:转达分组 包括&#…

导航系统架构及业务模块组合策略

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录 系列文章目录前言一、嵌入式硬件系统架构【开发系统平台架构】通讯方式及组件选型方向导航机器人硬件配置及其常用功能 二、嵌入式软件系统组件…

【ensp实验】Telnet 协议

目录 Telnet 协议 telnet协议特点 Telnet实验 ​编辑 不使用console口 三种认证模式的区别 Telnet 协议 Telnet 协议是 TCP/IP 协议族中的一员,是 Internet 远程登录服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用…

智能合约——提案demo

目录 这是一个超超超级简单的智能合约提案项目,你确定不点进来看一下吗? 引言: 1、搭建开发环境: 2、编写智能合约: 3、部署智能合约: ​编辑​编辑4、编写前端交互代码(使用web3.js&…

使用riscv-tests进行指令测试(二)

使用riscv-tests进行指令测试(二) 1 测试用例命名规则2 测试用例dump文件介绍 本文属于《 TinyEMU模拟器基础系列教程》之一,欢迎查看其它文章。 1 测试用例命名规则 用例名称 TVM Name “-” Target Environment Name “-” “指令”…

uniapp判断是图片还是pdf,如果是pdf则进行下载预览

一、附件中有图片也有pdf&#xff0c;需要进行预览&#xff0c;图片可直接预览&#xff0c;而pdf是下载后再预览 二、主要代码 <view class"fj-row" v-for"(item,index) in formDetail.attachmentRespVOS" :key"index"><view class&qu…

springboot常用注释

SpringBootApplication 标明启动类的注释&#xff0c;也就是标明项目程序入口&#xff0c;实际上集成了非常多的注释 SpringBootApplication public class SpringbootApplication {public static void main(String[] args) {SpringApplication.run(SpringbootApplication.cla…

区块链基础——区块链应用架构概览

目录 区块链应用架构概览&#xff1a; 1、区块链技术回顾 1.1、以太坊结点结构 1.2、多种应用场景 2、区块链应用架构概览 2.1、传统的Web2 应用程序架构 2.2、Web3 应用程序架构——最简架构 2.3、Web3 应用程序架构——前端web3.js ether.js 2.4、Web3 应用程序架构—…

react 实现自动创建api 请求文件

需求&#xff1a; 前后端分离的情况下前端要调用后端的接口要写很多接口调用的定义文件很繁琐&#xff0c;切没有意义都是体力劳动 进程&#xff1a; 让后端使用swagger 或者其他的openpai 格式的组件将server 端的接口喷出如果是swagger 的话一般会有一个口子 /v2/api-docs…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之六 简单进行人脸训练与识别

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之六 简单进行人脸训练与识别 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单人脸检测/识别实战案例 之六 简单进行人脸训练与识别 一、简单介绍 二、简单进行人脸训练与识别 1、LBPH…

【MATLAB源码-第198期】基于simulink的三相光伏并网仿真模拟。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 三相光伏并网系统是一种将太阳能转换为电能并将其馈入电网的系统。这个系统通常包括光伏阵列、逆变器&#xff08;包括其控制算法&#xff09;、滤波器、电网连接和监控系统。从上载的框图中可以看出&#xff0c;该系统的设计…

【力扣】16. 最接近的三数之和

16. 最接近的三数之和 题目描述 给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数&#xff0c;使它们的和与 target 最接近。 返回这三个数的和。 假定每组输入只存在恰好一个解。 示例 1&#xff1a; 输入&#xff1a;nums [-1,2,1…

Golang实现一个批量自动化执行树莓派指令的软件(6)简易批量指令处理

简介 基于上篇 Golang实现一个批量自动化执行树莓派指令的软件(5)模块整合&#xff0c; 这里我们实现简单的从配置文件设置指令集&#xff0c; 然后程序自动运行指令集的操作。 环境描述 运行环境: Windows&#xff0c; 基于Golang&#xff0c; 暂时没有使用什么不可跨平台接口…

找不到mfc140.dll如何解决?mfc140.dll丢失的几种解决方法分享

在我们启动并开始利用电脑进行日常工作的过程中&#xff0c;如果遭遇了操作系统提示“mfc140.dll文件丢失”的错误信息&#xff0c;导致某些应用程序无法正常运行&#xff0c;这究竟是何种情况呢&#xff1f;小编将介绍计算机缺失mfc140.dll文件的5种解决方法&#xff0c;帮助大…

java项目:微信小程序基于SSM框架实现的购物系统小程序【源码+数据库+毕业论文+PPT】

一、项目简介 本项目是一套基于SSM框架实现的购物系统小程序 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功能齐…

MATLAB初学者入门(17)—— 爬山算法

爬山算法是一种局部搜索算法&#xff0c;它采用贪心策略来迭代改进问题的解决方案&#xff0c;直到达到局部最优。爬山算法在解决一些优化问题时很有用&#xff0c;尤其是当问题的解空间是离散的&#xff0c;并且我们可以容易地定义“邻居”概念时。 案例分析&#xff1a;使用…

unity学习(91)——云服务器调试——补充catch和if判断

本机局域网没问题&#xff0c;服务器放入云服务器后&#xff0c;会出现异常。 想要找到上面的问题&#xff0c;最简单的方法就是在云服务器上下载一个vs2022&#xff01; 应该不是大小端的问题&#xff01; 修改一下readMessage的内容&#xff0c;可以直接粘贴到云服务器的。 …

使用FunASR处理语音识别

FunASR是阿里的一个语音识别工具&#xff0c;比SpeechRecognition功能多安装也很简单&#xff1b; 官方介绍&#xff1a;FunASR是一个基础语音识别工具包&#xff0c;提供多种功能&#xff0c;包括语音识别&#xff08;ASR&#xff09;、语音端点检测&#xff08;VAD&#xff…

DAC音频解码芯片DP7398立体声数模转换芯片

DP7398 Pin TO Pin CS4398和CS43122&#xff0c;同轴光纤DAC解码&#xff0c;支持HIFI播放器。 产品介绍 DP7398 是一个立体声 24 位/1 92kHz 数模转换芯片。 该 D/A 系统包括数字去加重、半分贝步长音量控制、 ATAP I 通道混频、可选择的快速和慢速数字插补滤波器和过采样多位…