Unity 结构少继承多组合

为什么不推荐使用继承?

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

假设我们要设计一个关于鸟的类。我们将“鸟类”这样一个抽象的事物概念,定义为一个抽象类 AbstractBird。所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。

我们知道,大部分鸟都会飞,那我们可不可以在 AbstractBird 抽象类中,定义一个 fly()方法呢?答案是否定的。尽管大部分鸟都会飞,但也有特例,比如鸵鸟就不会飞。鸵鸟继承具有 fly() 方法的父类,那鸵鸟就具有“飞”这样的行为,这显然不符合我们对现实世界中事物的认识。当然,你可能会说,我在鸵鸟这个子类中重写(override)fly() 方法,让它抛出 UnSupportedMethodException 异常不就可以了吗?具体的代码实现如下所示:

public class AbstractBird {
	//... 省略其他属性和方法...
	public void fly() { //... }
}

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

复制

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

总之,继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。这也是为什么我们不推荐使用继承。那刚刚例子中继承存在的问题,我们又该如何来解决呢?你可以先自己思考一下,再听我下面的讲解。

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

实际上,我们可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。

我们前面讲到接口的时候说过,接口表示具有某种行为特性。针对“会飞”这样一个行为特性,我们可以定义一个 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(); // 委托
	}
}

复制

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

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

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

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

除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。

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

复制

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

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);

复制

尽管有些人说,要杜绝继承,100% 用组合代替继承,但是我的观点没那么极端!之所以“多用组合少用继承”这个口号喊得这么响,只是因为,长期以来,我们过度使用继承。还是那句话,组合并不完美,继承也不是一无是处。只要我们控制好它们的副作用、发挥它们各自的优势,在不同的场合下,恰当地选择使用继承还是组合,这才是我们所追求的境界。

Unity自己的代码也是有继承,同时想想Unity的各种组件挂在同一个物体上,这就是组合的思想

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

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

相关文章

Qt双击某一文件通过自己实现的程序打开,并加载文件显示

双击启动 简述方法一方法二注意 简述 在Windows系统中,双击某类扩展名的文件,通过自己实现的程序打开文件,并正确加载及显示文件。有两种方式可以到达这个目的。 对于系统不知道的扩展名的文件,第一次打开时,需要自行…

Redis过期数据的删除策略

1 介绍 Redis 是一个kv型数据库,我们所有的数据都是存放在内存中的,但是内存是有大小限制的,不可能无限制的增量。 想要把不需要的数据清理掉,一种办法是直接删除,这个咱们前面章节有详细说过;另外一种就是…

MySQL卸载-Linux版

MySQL卸载-Linux版 停止MySQL服务 systemctl stop mysqld 查询MySQL的安装文件 rpm -qa | grep -i mysql 卸载上述查询出来的所有的MySQL安装包 rpm -e mysql-community-client-plugins-8.0.26-1.el7.x86_64 --nodeps ​ rpm -e mysql-community-server-8.0.26-1.el7.x86_64 -…

人机界面通过RJ45口无线连接多台PLC

人机界面是系统和用户之间进行交互和信息交换的媒介,它实现信息的内部形式与人类可以接受形式之间的转换。人机界面产品由硬件和软件两部分组成,硬件部分包括处理器、显示单元、输入单元、通讯接口、数据存贮单元等,HMI软件一般分为两部分&am…

网络安全(黑客)入门

想自学网络安全(黑客技术)首先你得了解什么是网络安全!什么是黑客! 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“安全…

【python利用shp文件进行绘图白化】

python白化 白化的作用python实现 白化的作用 参考博文【matlab利用shp文件制作mask白化文件】 python实现 python借助shp文件对绘图进行白化,不需要进行mask文件的制作,可以高效地进行区域绘制 import numpy as np import cartopy.crs as ccrs impo…

C++入门---vector常用函数介绍及使用

文章目录 vector的介绍vector的使用vector拷贝构造函数vector迭代器vector 空间增长vector的size、capacity、empty函数vector的resize函数vector的reserve函数 vector增删查改vector的push_back、pop_back函数vector的operator[]函数vector的insert、erase函数vector与算法关联…

MQ消息队列(主要介绍RabbitMQ)

消息队列概念&#xff1a;是在消息的传输过程中保存消息的容器。 作用&#xff1a;异步处理、应用解耦、流量控制..... RabbitMQ&#xff1a; SpringBoot继承RabbitMQ步骤&#xff1a; 1.加入依赖 <dependency><groupId>org.springframework.boot</groupId&g…

Springboot 项目配置Swagger2

1. 加入swagger 依赖 springboot 项目的 pom.xml 中添加下列依赖&#xff1a; <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version> </dependency> <depe…

C语言入门 Day_9 条件判断

目录 前言&#xff1a; 1.if判断 2.else判断 3.易错点 4.思维导图 前言&#xff1a; 我们知道比较运算和逻辑运算都会得到一个布尔型的数据&#xff0c;要么为真&#xff08;true&#xff09;&#xff0c;要么为假&#xff08;false&#xff09;。 今天我们来学习真和假在…

综合能源系统(8)——综合能源系统支撑技术

综合能源系统关键技术与典型案例  何泽家&#xff0c;李德智主编 1、大数据技术 1.1、大数据技术概述 大数据是指无法在一定时间范围内用常规软件工具进行捕捉、管理和处理的数据集合&#xff0c;是需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量、高…

Firefox(火狐),使用技巧汇总,问题处理

本文目的 说明火狐如何安装在C盘之外的盘&#xff0c;即定制安装路径。如何将同步功能切换到本地服务上。默认是国际服务器。安装在C盘之后如何解决&#xff0c;之前安装的扩展无法自动同步的问题。顺带讲解一下&#xff0c;火狐的一些比较好用的扩展。 安装路径定制 火狐目前…

Java IO流(五)Netty实战[TCP|Http|心跳检测|Websocket]

Netty入门代码示例(基于TCP服务) Server端 package com.bierce.io.netty.simple; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGro…

宝塔 杀死 java服务 netstat -tlnp | grep :7003 kill 2205698

7003 是端口 netstat -tlnp | grep :7003 kill 2205698

企业电子招投标采购系统源码之电子招投标的组成 tbms

​ 功能模块&#xff1a; 待办消息&#xff0c;招标公告&#xff0c;中标公告&#xff0c;信息发布 描述&#xff1a; 全过程数字化采购管理&#xff0c;打造从供应商管理到采购招投标、采购合同、采购执行的全过程数字化管理。通供应商门户具备内外协同的能力&#xff0c;为…

七夕特辑(一)浪漫表白方式 用神经网络生成一首情诗

目录 一、准备工作二、用神经网络生成一首诗&#xff0c;代码说明 牛郎织女相会&#xff0c;七夕祝福要送来。祝福天下有情人&#xff0c;终成眷属永相伴。 七夕是中国传统的情人节&#xff0c;也是恋人们表达爱意的好时机。在这个特别的日子里&#xff0c;送上温馨的祝福&…

STM32电源名词解释

STM32电源架构 常用名词 VCC Ccircuit 表示电路&#xff0c;即接入电路的电压。 VDD Ddevice 表示器件&#xff0c; 即器件内部的工作电压。 VSS Sseries 表示公共连接&#xff0c;通常指电路公共接地端电压。 VDDA Aanalog 表示模拟&#xff0c;是模拟电路部分的电源。主要为…

Java“牵手”天猫商品列表数据,关键词搜索天猫商品数据接口,天猫API申请指南

天猫商城是一个网上购物平台&#xff0c;售卖各类商品&#xff0c;包括服装、鞋类、家居用品、美妆产品、电子产品等。要获取天猫商品列表和商品详情页面数据&#xff0c;您可以通过开放平台的接口或者直接访问天猫商城的网页来获取商品详情信息。以下是两种常用方法的介绍&…

21. 合并两个有序链表(简单系列)

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4] 示例 2&#xff1a; 输入&#xff1a;l1 [], l2 [] 输出&#xff1a;[] 示例 3&#xff1a; …

jstat(JVM Statistics Monitoring Tool):虚拟机统计信息监视工具

jstat&#xff08;JVM Statistics Monitoring Tool&#xff09;&#xff1a;虚拟机统计信息监视工具 用于监视虚拟机各种运行状态信息的命令行工具。 它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据&#xff0c;在没有GUI图形界面、只提…
最新文章