面向对象编程中类与类之间的关系(一)
目录
1.引言
2."有一个"关系
3."是一个"关系(继承)
4.“有一个”与“是一个”的区别
5.not-a关系
6.层次结构
7.多重继承
8.混入类
1.引言
作为程序员,必然会遇到这样的情况:不同的类具有共同的特征,至少看起来彼此有联系。面向对象的语言提供了许多机制来处理类之间的这种关系。最棘手的问题是理解这些关系的实质。类之间的关系主要有两类:"有一个"(has a)关系和"是一个"(is a)关系。
2."有一个"关系
"有一个"关系模式是A有一个B,或者A包含一个B。在此类关系中,可认为某个类是另一个类的一部分。前面定义的组件通常代表"有一个"关系,因为组件表示组成其他类的类。
动物园和猴子就是这种关系的一个示例。可以说动物园里有一只猴子,或者说动物园包含一只猴子。在代码中用Zoo对象模拟动物园,这个对象有一个Monkey组件。
考虑用户界面通常有助于理解类之间的关系。尽管并非所有的UI都是(虽然现在大多数都是)以OOP方式实现的,屏幕上的视觉元素也能很好地转换为类。UI关于“有一个”关系的类比就是窗口中包含一个按钮,按钮和窗口是两个明显不同的类,但又明显有某种关系。由于按钮在窗口中,因此窗口中有一个按钮。
下图显示了实际的“有一个”关系和用户界面的“有一个”关系。
"有一个"关系有如下两种类型:
1)聚合:通过聚合,当聚合器被销毁时,聚合对象(组件)可以继续存在。例如,假如动物园对象包含一组动物对象。当动物园对象因为破产而被销毁时,动物对象(理想情况下) 不会被销毁,他们被转移到另外一个动物园。
2)组合:对于组合,如果由其他对象组成的对象被销毁,那么这些其他对象也会被销毁。例如:如果包含按钮的窗口对象被销毁,则这些按钮对象也将被销毁。
3."是一个"关系(继承)
“是一个”关系是面向对象编程中非常基本的概念。因此有许多名称,包括派生(deriving)、子类(subclass)、扩展(extending)和继承(inheriting)。类模拟了显示世界包含具有属性和行为的对象这一事实,继承模拟了这些对象通常以层次方式来组织这一事实。“是一个”说明了这种层次关系。
基本上,继承的模式是:A是一个B,或者A实际上与B非常相似-----这可能比较棘手。再次以简单的动物园为例,但假定动物园里除了猴子之外还有其他动物。这句话本身已经建立了关系-----猴子是一种动物。同样,长颈鹿也是一种动物,袋鼠是一种动物,企鹅也是一种动物。那又怎么样?意识到猴子、长颈鹿、袋鼠和企鹅具有某种共性时,继承的魔力就开始显现了。这些共性就是动物的一般特征。
对于程序员的启示就是,可定义Animal类,用于封装所有动物的属性(大小、生活区域、食物等)和行为(走动、进食、睡觉)。特定的动物(例如猴子)成为Animal的子类,因为猴子包含动物的所有特征。记住,猴子是动物,并且它还有与众不同的其他特征。动物的继承关系如下图所示:
就像猴子与长颈鹿是不同类型的动物一样,用户界面中通常也有不同类型的按钮。例如,复选框是一个按钮,按钮只是一个可被单击并执行操作的UI元素,Checkbox类通过添加状态(相应的框是否被选中)扩展了Button类。
当类之间具有“是一个”关系时,目标之一就是将通用功能放入基类(base class),其它了可扩展基类。如果所有子类都有相似或完全相同的代码,就应该考虑将一些代码或全部代码放入基类。这样,可在一个地方完成所需的改动,将来的子类可“免费”获取这些共享的功能。
1.继承技术
前面的示例非正式地讲述了继承中使用的一些技术。当生成子类时,程序员有多种方法将某个类与其父类(也称为基类或超类)区分开。可使用多种方法生成子类,生成子类实际上就是完成语句A is a B that...的过程。
添加功能
派生类可在基类的基础上添加功能。例如,猴子是一种可以在树间荡秋千的动物。除了具有动物的所有行为以外,Monkey类还有swingFromTrees0方法,这个行为只存在于Monkey 类中。
替换功能
派生类可完全替换或重写父类的行为。例如,大多数动物都步行,因此 Animal类可能拥有模拟步行的 move 行为。但袋鼠是一种通过跳跃而不是步行移动的动物,Animal 基类的其他属性和行为仍然适用,Kangaroo 派生类只需要改变 move 行为的运行方式。当然,如果对基类的所有功能都进行替换,就可能意味着采用继承的方式根本就不正确,除非基类是一个抽象基类。抽象基类会强制每个子类实现未在抽象基类中实现的所有方法。
添加属性
除了从基类继承属性以外,派生类还可添加新属性。企鹅具有动物的所有属性,此外还有 beaksize(鸟喙大小)属性。
替换属性
与重写方法类似,C++提供了重写属性的方法。然而,这么做通常是不合适的,因为这会隐藏基类的属性。也就是说,基类可为具有特定名称的属性指定一个值,而派生类可用同一个名字为另一个属性指定另一个值。有关“隐藏”的内容,会在后面讲解。不要把替换属性的概念与子类具有不同属性值的概念混淆。例如,所有动物都具有表明它们吃什么的 diet 属性,猴子吃香蕉,企鹅吃鱼,二者都没有替换 diet 属性--只是赋给属性的值不同而已。
2.多态性
多态性(Polymorphism)指遵循一套标准属性和方法的对象可互换使用。类定义就像对象和与之交互的代码之间的契约。根据定义,任意一个Monkey 对象必须支持 Monkey 类的属性和行为。
这个概念也可推广到基类、由于所有猴子都是动物,因此所有Mokey对象都支转属性和行为。
多态性是面向对象编程的亮点,因为多态性真正利用了继承所提供的能力、在模拟动物园时,可通过编程遍历动物园的所有动物,让每个动物都移动一次。由于所有动物都是Animal类的成员,因此它们都知道如何移动。某些动物重写了移动行为,但这正是亮点所在一一代码只是告诉每个动物移动,而不知道也不关心是哪种动物。所有动物都按自己的方式移动。
4.“有一个”与“是一个”的区别
在现实中,区分对象之间的“有一个”与“是一个”关系相当容易。没有人会说橘子有一个水果-----橘子是一种水果。在代码中,有时并不会那么明显。
下面举个例子来说明这个问题。
假设我们有一个名为AssociativeArray的类,用于高效地将键映射到值。例如,保险公司可以使用AssociativeArray类来将会员ID映射到名字。同时,我们还想要一个可以允许一个键对应多个值的数据结构。这个需求导致了另一个类MultiAssociativeArray的产生。
MultiAssociativeArray类在功能上与AssociativeArray非常相似,除了它允许多值与单一键关联。这引出了一个设计问题:MultiAssociativeArray应该是AssociativeArray的派生类(Is-a关系)还是应该包含一个AssociativeArray对象(Has-a关系)?
Is-a关系(继承)的代码示例:
// 基类 AssociativeArray
class AssociativeArray {
public:void insert(int key, std::string value);std::string get(int key);// ... 其他方法
};// 派生类 MultiAssociativeArray
class MultiAssociativeArray : public AssociativeArray {
public:void insert(int key, std::string value) override;std::string get(int key) override;std::vector<std::string> getAll(int key);// ... 其他方法
};
Has-a关系(组合)的代码示例:
// 基类 AssociativeArray
class AssociativeArray {
public:void insert(int key, std::string value);std::string get(int key);// ... 其他方法
};// 使用组合的 MultiAssociativeArray
class MultiAssociativeArray {
private:AssociativeArray internalArray; // 内部对象
public:void insert(int key, std::string value);std::vector<std::string> getAll(int key);// ... 其他方法
};
关系类型 | 优点 | 缺点 |
Is-a(继承) | - 能够复用AssociativeArray 的所有功能和代码- 可以用 AssociativeArray 对象的地方也能使用MultiAssociativeArray 对象 | - get() 方法需要重写以返回一个特定的值,这可能会引入复杂性- 如果 AssociativeArray 类的实现发生变化,可能需要更改MultiAssociativeArray |
Has-a(组合) | - 更灵活,因为可以更容易地修改或扩展MultiAssociativeArray 的功能- 不必担心 AssociativeArray 的任何功能或方法“渗透”到MultiAssociativeArray | - 可能需要编写更多的代码来调用内部AssociativeArray 对象的方法 |
LSP建议,派生类对象应该能够替换其基类对象,而不改变程序的正确性。在这个案例中,由于MultiAssociativeArray
的insert()
方法与AssociativeArray
的行为不同(不会删除具有相同键的早期值),因此更倾向于Has-a关系。
基于上述分析,推荐使用Has-a关系。这样,MultiAssociativeArray
可以有自己独立的接口和实现,同时还可以灵活地应对未来需求的变化。
5.not-a关系
当考虑类之间的关系时,应该考虑类之间是否真的存在关系。不要把对面向对象设计的热情全部转换为许多不必要的类/子类关系。
当实际事物之间存在明显关系,而代码中没有实际关系时,问题就出现了。OO(面向对象)层次结构需要模拟功能关系,而不是人为的制造关系。下图显示的关系作为概念集或层次结构是有意义的,但是代码中并不能代表有意义的关系。
避免不必要继承的最好方法是首先给出大概得设计。为每个类和派生类写出计划设置的属性和方法。如果发现某个类没有自己特定的属性或方法,或者某个类的所有属性和方法都被派生类重写,只要这个类不是前面提到的抽象基类,就应该重新考虑设计。
6.层次结构
正如类A可以是类B的基类一样,B也可以是C的基类。面向对象层次结构可模拟类似的多层关系。一个具有多种动物的动物园模拟程序,可能会将每种动物作为 Animal 类的子类,如下图所示。
当编写每个派生类的代码时,许多代码可能是相似的。此时,应该考虑让它们拥有共同的父类。Lionhe Panther的移动方式和食物相同,说明可使用BigCat类。还可进一步将 Animal类细分,以包括WaterAnimal和Marsupial,下图展示了利用这种共性的更趋层次化的设计。
生物学家看到这个层次结构可能会失望----海豚(Dolphin)和企鹅(Ppngui)并不属于同一科。然而,这强调了一个要点----在代码中,需要平衡现实关系和共享功能关系。即使现实中两种事物紧密联系,在代码中也可能没有任何关系,因此它们没有共享功能。可简单地把动物分为哺乳动物和鱼类,但是这会使基类没有任何共同因素。
另一个要点是可用其他方法创建这个层次结构。前面的设计基本上根据动物的移动方式创建的。如果根据动物吃的食物或身高创建,层次结构可能会大不相同。总之,关键在于如果使用类,需求决定对层次结构的设计。
优秀的面向对象层次结构能做到以下几点:
1)使类之间存在有意义的功能联系。
2)将共同的功能放入基类,从而支持代码重用。
3)避免子类过多地重写父类的功能,除非父类是一个抽象类。
7.多重继承
到目前为止,所有示例都只有单一的继承链。换句话说,给定的类最多只有一个直接的父类。事物并非一直如此,在多重继承中,一个类可以有多个基类。
下图给出了一种多重继承设计。在此任然有一个基类Animal, 根据大小对这个类进行细分。此外根据食物划分了一个独立的层次类别,根据移动方式又划分了一个层次类别;所有类型的动物都是这三个类的子类。
考虑用户界面环境,假定用户可单击某张图片。这个对象好像既是按钮又是图片,因此其实现同时继承了 Image 类和 Button 类,如下图所示。
某些情况下多重继承可能很有用,但必须记住它也有很多缺点。许多程序员不喜欢多重继承,C++明确支持这种关系,而Java语言根本不予支持,除非通过多个接口来继承(抽象基类)。多重继承的批评者一般有以下几个原因。
首先,用图形表示多重维承十分复杂,如上图所示,当存在多重维承和交叉线时,即使简单的类图也会变得非常复系。类层次结构旨在让程序员更方便地理解代码之间的关系。而多种继承中,类可有多个彼此没有关系的父类。将这么多的类加入对象的代码中,能跟踪发生了什么吗?
其次,多重维承会破坏清晰的层次结构。在动物示例中,使用多重继承方法意味着Animal基类作用降低,因为描述动物的代码现在分成三个独立的层次。尽管上图的设计显示了三个清晰的层次,但不难想象它们会变得如何凌乱。例如,如果发现所有的Jumper不仅以同样的方式移动,还吃同样的食物,该怎么办?由于层次是独立的,因此无法在不添加其他派生类的情况下加入移动和食物的概念。
最后,多重维承的实现很复杂、如果两个基类以不同方式实现了相同的行为,该怎么办?两个基类本身是同一个基类的子类,可以这样吗?这种可能让实现变得复杂,因为在代码中建立这样复杂的关系会给作者和读者带来挑战。
其他语言取消多重继承的原因是,通常可以避免使用多重继承。在设计某个项目时,重新考虑层次结构,通常可避免引入多重继承。
8.混入类
混入(mixin)类代表类之间的另一种关系。在 C++中,混入类的语法类似于多重继承,但语义完全不同,混入类回答“这个类还可以做什么”这个问题,答案经常以“-able”结尾。使用混入类,可向类中添加功能,而不需要保证是完全的“是一个”关系。可将它当作一种分享(share-with)关系。
回到动物园示例,假定想引入某些动物可以“被抚摸”这一概念。也就是说,动物园的游客可以抚模一些动物,大概率不会被咬伤或伤害。所有可以被抚摸的动物支持“被抚摸”行为。由于可以被抚模的动物没有其他的共性,且不想破坏已经设计好的层次结构,因此Pettable 就是很好的混入类。
混入类经常在用户界面中使用。可以说 Image 能够点击(Clickable),而不需要说 PictureButton 类既是 lmage 又是 Button。桌面上的文件夹图标可以是一张可拖动(Draggable)、可点击(Clickable)的图片(lmage)。软件开发人员总是喜欢弄一大堆有趣的形容词。
混入类和基类之间的区别更多地取决于对类的看法,而不是代码的区别。因为范围有限,混入类通常比多重层次结构容易理解。Pettable混入类只是在已有类中添加了一个行为,Clickable混入类或许仅添加了“按下鼠标”和“释放鼠标”行为。此外,混入类很少会有庞大的层次结构,因此不会出现功能的交叉混乱。
推荐阅读:
组合优于继承:什么情况下可以使用继承?
相关文章:

面向对象编程中类与类之间的关系(一)
目录 1.引言 2."有一个"关系 3."是一个"关系(继承) 4.“有一个”与“是一个”的区别 5.not-a关系 6.层次结构 7.多重继承 8.混入类 1.引言 作为程序员,必然会遇到这样的情况:不同的类具有共同的特征,至少看起来彼…...

streamlit 实现 flink SQL运行界面
实现效果 streamlit flink-playground.py 文件如下: import streamlit as st import io import contextlib import sys import os import uuid import subprocess from jinja2 import Templatest.set_page_config(layout"wide")# 设置页面标题 st.title…...

鲸鱼优化算法(Whale Optimization Algorithm, WOA)原理与MATLAB例程
鲸鱼优化算法(Whale Optimization Algorithm, WOA)是一种基于鲸鱼捕食行为的智能优化算法。它模拟了座头鲸在狩猎时的“气泡网”捕食策略。 文章目录 1.适应度函数2. 更新公式2.1 突袭行为2.2 螺旋更新3.线性递减参数4. 边界处理 MATLAB 实现示例代码说明…...

MFC七段码显示实例
在MFC中添加iSenvenSegmentAnalogX控件,添加编辑框和按钮实现在编辑框中输入数字点击按钮后数字用七段码显示 1、在对话框中点击右键如下图添加控件和变量 2、在sevenDlg.h中添加代码 public: void ShowInd(int,double);3、在sevenDlg.cpp中添加代码 void CSe…...

【日常知识点】到底推不推荐用JWT?
👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO 专家博主 ⛪️ 个人社区:个人社区 💞 个人主页:个人主页 🙉 专栏地址: ✅ Java 中级 🙉八股文专题:剑指大厂,手撕 J…...
网络编程项目之FTP服务器
项目介绍 模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在࿰…...

SpringBoot02:第一个springboot程序
3、第一个springboot程序 3.1、准备工作 我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。 我的环境准备: java version "…...

快速入门HTML
欢迎关注个人主页:逸狼 创造不易,可以点点赞吗 如有错误,欢迎指出~ 目录 第一个html文件 标签 h1~h6 p >段落标签 br > 换行标签 img >图片标签 a >超链接标签 表格标签 表单标签 表单控件 form表单 ⽆语义标签:div&span 综…...

RabbitMQ是一个开源的消息代理和队列服务器
RabbitMQ是一个开源的消息代理和队列服务器,它基于AMQP(Advanced Message Queuing Protocol,高级消息队列协议)协议实现,同时也支持其他消息协议如STOMP、MQTT等。作为一个可靠的消息传递服务,RabbitMQ在分…...
经典算法思想--并查集
前言 (最近在学习Java,所有函数都是用Java语言来书写的)前言部分是一些前提储备知识 在并查集(Union-Find)数据结构中,rank(中文称为“秩”)是用来表示树的高度或深度的一种辅助信息…...
挑战Java面试题复习第2天,百折不挠
挑战第 2 天 ArrayList和linkedList的区别HashMap和HashTable的区别Collection 与 Collections 的区别Java的四种引用泛型常用特点 ArrayList和linkedList的区别 底层数据结构: ArrayList:基于动态数组实现,支持快速随机访问。LinkedList&a…...

【vue之道】
vue之道 1. 一生二,二生万物思想2. 变化之律3. 变化之实在哪?4.而后学于形乃已!4.1 展示之形变4.2 动之气谓之指令4.3 血之养分的载体,于vue之绑定载具4.4 vue之道(万法规一篇) 1. 一生二,二生万…...

基于麻雀优化算法SSA的CEEMDAN-BiLSTM-Attention的预测模型
往期精彩内容: 时序预测:LSTM、ARIMA、Holt-Winters、SARIMA模型的分析与比较 全是干货 | 数据集、学习资料、建模资源分享! EMD、EEMD、FEEMD、CEEMD、CEEMDAN的区别、原理和Python实现(一)EMD-CSDN博客 EMD、EEM…...

Linux:指令再认识
文章目录 前言一、知识点1. Linux下一切皆文件,也就是说显示器也是一种文件2. 指令是什么?3. ll 与 ls -l4. 日志5. 管道6. 时间戳 二、基本指令1. man指令2. cp指令3. mv指令4. 查看文件1)cat/tac指令——看小文件2)more/less指令…...
PHP如何抛出和接收错误
在PHP中,抛出和接收错误通常涉及异常处理机制,以及错误和异常的处理函数。以下是如何在PHP中抛出和接收错误的详细指南: 抛出错误(异常) 在PHP中,你可以使用throw关键字来抛出一个异常。这通常在你检测到…...

计算机网络:网络层 —— IPv4 地址的应用规划
文章目录 IPv4地址的应用规划定长的子网掩码变长的子网掩码 IPv4地址的应用规划 IPv4地址的应用规划是指将给定的 IPv4地址块 (或分类网络)划分成若干个更小的地址块(或子网),并将这些地址块(或子网)分配给互联网中的不同网络,进而可以给各网络中的主机…...
Mongodb命令大全
Mongodb命令大全 一、数据库相关命令二、集合相关命令三、文档(数据)相关命令1、_id 字段说明2、查询2.1、 查询操作符2.2、内嵌文档查询2.3、数组文档查询2.4、去重查询2.5、查询排序 sort2.6、分页查询2.7、指定列投影查询返回2.8、查询统计个数 count 3、聚合查询3.1、查询用…...

宇视设备视频平台EasyCVR视频融合平台果园/鱼塘/养殖场/菜园有电没网视频监控方案
在那些有电无网的偏远地区,如果园、鱼塘、养殖场或菜园,视频监控的实现面临着独特的挑战。宇视设备视频平台EasyCVR提供了一种创新的解决方案,通过结合太阳能供电和4G摄像头技术,有效地解决了这些场景下的监控需求。 在有电没网的…...
面试题:ABCD四个线程,A线程最后执行
我觉得是一个很高频的面试题,ABCD四个线程,A线程要等到BCD线程执行完再执行,怎么做 因为我刚复习完AQS,所以立马想到了CountDownLatch,但是看面试官反应他最想听到的应该是join方法,所以面试后就总结了几种…...

代码随想录算法训练营第46期Day43
leetcode.322零钱兑换 class Solution { public: //无限个硬币->完全背包int coinChange(vector<int>& coins, int amount) {vector<int> dp(10010,INT_MAX);//dp代表的在某个数值下最小的硬币数,要求是最小的硬币数,所以初始值要尽可…...

业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...

C++实现分布式网络通信框架RPC(3)--rpc调用端
目录 一、前言 二、UserServiceRpc_Stub 三、 CallMethod方法的重写 头文件 实现 四、rpc调用端的调用 实现 五、 google::protobuf::RpcController *controller 头文件 实现 六、总结 一、前言 在前边的文章中,我们已经大致实现了rpc服务端的各项功能代…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...

微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
数据库分批入库
今天在工作中,遇到一个问题,就是分批查询的时候,由于批次过大导致出现了一些问题,一下是问题描述和解决方案: 示例: // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

【OSG学习笔记】Day 16: 骨骼动画与蒙皮(osgAnimation)
骨骼动画基础 骨骼动画是 3D 计算机图形中常用的技术,它通过以下两个主要组件实现角色动画。 骨骼系统 (Skeleton):由层级结构的骨头组成,类似于人体骨骼蒙皮 (Mesh Skinning):将模型网格顶点绑定到骨骼上,使骨骼移动…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...