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

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) 分组交换设备:转达分组 包括&#…

【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 “-” “指令”…

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

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

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

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

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

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

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

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

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

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

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

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

使用FunASR处理语音识别

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

【Java数据结构】初步认识ArrayList与顺序表

前言~🥳🎉🎉🎉 hellohello~,大家好💕💕,这里是E绵绵呀✋✋ ,如果觉得这篇文章还不错的话还请点赞❤️❤️收藏💞 💞 关注💥&#x…

分布式文件系统--MinIO

1 MinIO安装(Docker) ●在root目录下新建docker_minio文件夹 ●在docker_minio文件夹下新建config文件夹,data文件夹 ●在root目录下新建docker_compose文件夹,在docker_compose文件夹中添加docker-compose.yaml services:minio:image: quay.io/minio/miniocontainer_name: mi…

Vuforia AR篇(三)— AR模型出场效果

目录 前言一、AR模型出场二、AR出场特效三、添加过渡效果四、效果 前言 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。 一、AR模型出场 创建ARCamer…

Three.js——基础材质、深度材质、法向材质、面材质、朗伯材质、Phong材质、着色器材质、直线和虚线、联合材质

个人简介 👀个人主页: 前端杂货铺 🙋‍♂️学习方向: 主攻前端方向,正逐渐往全干发展 📃个人状态: 研发工程师,现效力于中国工业软件事业 🚀人生格言: 积跬步…

【树莓派】强力烧写工具 Balena Etcher,烧写树莓派系统,树莓派系统克隆,备份

文章目录 使用Win32DiskImager备份和写入树莓派系统步骤一:下载和安装Win32DiskImager步骤二:准备工作步骤三:备份树莓派系统步骤四:写入树莓派系统 使用Balena Etcher给树莓派烧写系统Balena Etcher简介步骤一:下载Ba…

Mac安装telnet

一、安装Homebrew 1、打开官网:Homebrew — The Missing Package Manager for macOS (or Linux) 2、打开终端输入: /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 二、安装Telnet bre…

【LAMMPS学习】八、基础知识(4.5)TIP5P水模型

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语,以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

园区智慧化转型新篇章:解码智慧技术如何助力园区实现精细化管理,提升运营效率

目录 一、智慧技术概述及其在园区管理中的应用 (一)物联网技术的应用 (二)大数据技术的应用 (三)云计算技术的应用 二、智慧技术助力园区实现精细化管理 (一)实现资源优化配置…