面向对象编程中类与类之间的关系(一)

目录

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建议,派生类对象应该能够替换其基类对象,而不改变程序的正确性。在这个案例中,由于MultiAssociativeArrayinsert()方法与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混入类或许仅添加了“按下鼠标”和“释放鼠标”行为。此外,混入类很少会有庞大的层次结构,因此不会出现功能的交叉混乱。

推荐阅读:

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

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

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

相关文章

JavaWeb——Web入门(1/9)-Spring Boot Web介绍(Spring家族,Spring Boot)

目录 Spring家族 Spring Boot 在我们了解完了 Maven 这款项目构建工具的基本使用之后&#xff0c;接下来我们正式的进入到 Web 后端开发的学习。 第一篇章要了解的是 Spring Boot Web 的入门。 在正式开始之前&#xff0c;我们先需要介绍一下什么是 Spring 以及什么是 Spri…

H3C Hybrid 实验

实验拓扑 图 1-1 注&#xff1a;如无特别说明&#xff0c;描述中的 R1 或 SW1 对应拓扑中设备名称末尾数字为 1 的设备&#xff0c;R2 或 SW2 对应拓扑中设备名称末尾数字为 2 的设备&#xff0c;以此类推&#xff1b;另外&#xff0c;同一网段中&#xff0c;IP 地址的主机位为…

【NOI】C++函数入门二(自定义函数)

文章目录 前言一、概念1.导入1.1 首先什么是函数呢&#xff1f; 2.函数分类3.为什么要定义函数呢&#xff1f;4.函数结构5.函数使用注意事项 二、例题讲解问题&#xff1a;1137 - 纯粹素数问题&#xff1a;1258 - 求一个三位数问题&#xff1a;1140 - 亲密数对问题&#xff1a;…

Flutter仿京东商城APP实战 用户中心基础布局

用户中心界面 pages/tabs/user/user.dart import package:flutter/material.dart; import package:jdshop/utils/zdp_screen.dart; import package:provider/provider.dart;import ../../../store/counter_store.dart;class UserPage extends StatefulWidget {const UserPage…

如何在Node.js中执行解压缩文件操作

一、解压文件 1.安装依赖&#xff1a; 安装adm-zip依赖包&#xff1a;npm install adm-zip --save 安装iconv-lite依赖包&#xff1a;npm install iconv-lite --save 解压前的file文件夹结构&#xff1a; update-1.0.2.zip压缩包内容&#xff1a; 2.在depresssFile.js文件&…

Vue学习笔记(六)

模板引用(获取DOM 操作) 虽然Vue的声明性渲染模型为你抽象了大部分对DOM的直接操作&#xff0c;但在某些情况下&#xff0c;我们仍然需要直接访问底层DOM元素。要实现这一点&#xff0c;我们可以使用特殊的refattribute。 挂载结束后引用都会被暴露在this.$refs之上。 <s…

华为原生鸿蒙操作系统:我国移动操作系统的新篇章

华为原生鸿蒙操作系统&#xff1a;我国移动操作系统的新篇章 引言 在移动操作系统领域&#xff0c;苹果iOS和安卓系统一直占据主导地位。然而&#xff0c;随着华为原生鸿蒙操作系统的正式发布&#xff0c;这一格局正在发生深刻变化。作为继苹果iOS和安卓系统后的全球第三大移动…

android studio编译错误提示无法下载仓库

一、调整方法之一 buildscript {repositories {google()jcenter()//maven { url https://maven.aliyun.com/repository/google }//maven { url https://maven.aliyun.com/repository/central }}dependencies {// classpath "com.android.tools.build:gradle:4.1.1"c…

Prompt Engineering (Prompt工程)

2 prompt工程2大原则 2.1 给出清晰&#xff0c;详细的指令 策略1&#xff1a;使用分割符清晰的指示输出的不同部分&#xff0c;比如"",<>,<\tag>等分隔符 策略2&#xff1a;指定一个结构化的输出&#xff0c;比如json,html等格式 策略3&#xff1a;要…

C++STL之stack

1.stack的使用 函数说明 接口说明 stack() 构造空的栈 empty() 检测 stack 是否为空 size() 返回 stack 中元素的个数 top() 返回栈顶元素的引用 push() 将元素 val 压入 stack 中 pop() 将 stack 中尾部的元素弹出 2.stack的模拟实现 #include<vector> namespace abc { …

监控-08-skywalking监控告警

文章目录 前言一、准备二、配置skywalking2.1 修改alarm-settings.yml2.2 重启skywalking 三、收到告警邮件总结 前言 skywalking根据监控规则&#xff0c;通过webhook调后端微服务接口&#xff0c;从而发送告警邮件。 一、准备 根据上几章内容&#xff0c;保证skywalking能监…

Spring IoC DI

博主主页: 码农派大星. 数据结构专栏:Java数据结构 数据库专栏:MySQL数据库 JavaEE专栏:JavaEE 关注博主带你了解更多数据结构知识 目录 1. 应用分层 1.1 如何分层: 1.2 MVC与三层架构区别联系 2. Spring 3.IoC & DI⼊⻔ 3.1 什么是IoC&#xff1f; 3.2 DI 介绍 …

CANFD SSP第二采样点引发的“风波”分析

案例背景&#xff1a; 近几年来&#xff0c;主机厂逐渐大范围使用CANFD通信。在CAN网络中&#xff0c;因SSP第二采样点引发的错误帧偶有发生&#xff0c;所以在主机厂的SPEC需求中&#xff0c;明确要求启用CAN控制器的TDC收发器延迟补偿&#xff0c; 目录 1 CANFD的SSP第二采…

【设计模式】《Java 设计模式魔法:解锁高效编程的秘密武器》

标题&#xff1a;《Java 设计模式奇幻之旅&#xff1a;解锁高效编程的魔法钥匙》 摘要&#xff1a; 本文将深入探讨 Java 中的十种设计模式&#xff0c;包括单例模式、工厂方法模式、抽象工厂模式…迭代器模式、组合模式、模板方法模式等。通过详细的解释、生动有趣的例子以及…

【skywalking 】选择Elasticsearch存储

介绍 skywalking支持 Elasticsearch 和 OpenSearch 作为存储。 OpenSearch 是 ElasticSearch 7.11 的一个分支&#xff0c;但在 Apache 2.0 中获得许可。 OpenSearch 存储与 ElasticSearch 共享相同的配置。为了激活 OpenSearch 作为存储&#xff0c;请将存储提供程序设置为e…

Python4

4. 更多控制流工具 除了刚介绍的 while 语句&#xff0c;Python 还用了一些别的。我们将在本章中遇到它们。 4.1. if 语句 if elif else if x<0: x 0 print(Negative changed to zero) elif x0: print( zero) else: print(More) 4.2. for 语句 Pyth…

Mybatis的关联关系-多对多

在进行数据库原理的时候&#xff0c;我们将E-R图的实体转化为我们的表时&#xff0c;有时要考虑到多对多的关系。比如下图&#xff1a; 我们可以转化为下面的表&#xff1a; 因为User和Orders是1:n的关系&#xff0c;所以Orders有一个外键。 t_orders表 idnumberuser_id(外键…

uniapp使用easyinput文本框显示输入的字数和限制的字数

uniapp使用easyinput文本框显示输入的字数和限制的字数 先上效果图&#xff1a; 整体代码如下&#xff1a; <template><view class"nameInfoContent"><uni-easyinput class"uni-mt-5" suffixIcon"checkmarkempty" v-model&quo…

Redis 事务 总结

前言 相关系列 《Redis & 目录》&#xff08;持续更新&#xff09;《Redis & 事务 & 源码》&#xff08;学习过程/多有漏误/仅作参考/不再更新&#xff09;《Redis & 事务 & 总结》&#xff08;学习总结/最新最准/持续更新&#xff09;《Redis & 事务…

基础数据结构及算法——AVL树【自平衡二叉搜索树】解决失衡

历史 AVL 树是一种自平衡二叉搜索树&#xff0c;由托尔哈斯特罗姆在 1960 年提出并在 1962 年发表。它的名字来源于发明者的名字&#xff1a;Adelson-Velsky 和 Landis&#xff0c;他们是苏联数学家&#xff0c;于 1962 年发表了一篇论文&#xff0c;详细介绍了 AVL 树的概念和…