对象组合
本节阅读量:对象组合
在现实生活中,复杂对象通常由更小、更简单的对象构成。例如,汽车由金属框架、发动机、轮胎、变速器、方向盘和大量其他零件组成。计算机由CPU、主板、内存等部件构成……人也由较小的部分组成:头部、身体、腿、手臂等等。从较简单的对象构建复杂对象的过程称为对象组合。
广义地说,对象组合用于建模两个对象之间的 “有一个” 关系。汽车”有一个“变速器。您的计算机“有一个”CPU。你”有一颗“心。复杂对象有时称为整体或父对象,较简单的对象通常称为零件、子对象或组件。
在C++中,结构体和类可以具有各种类型的数据成员(例如基本类型或其他类)。当我们用数据成员构建类时,本质上是在用简单的部分构造复杂的对象,这就是对象组合。由于这个原因,结构体和类被称为复合类型。
对象组合在C++中非常有用,因为它允许我们通过组合更简单、更容易管理的部分来创建复杂的类。这降低了复杂度,让我们可以更快地编写代码,也更少出错,因为我们可以复用已经编写、测试并验证有效的代码。
对象组合的类型
对象组合有两个基本的子类型:组合和聚合。我们将在本课中研究组合,在下一节课中研究聚合。
术语注释:术语“组合”通常用于同时指代组合和聚合,而不仅仅指组合类型。在本教程中,当同时指代这两者时,我们将使用术语“对象组合”;当专门指代组合这一子类型时,我们将使用术语“组合”。
组合
要符合组合条件,对象和零件必须具有以下关系:
- 部件(成员)是对象(类)的一部分
- 部件(成员)一次只能属于一个对象(类)
- 部件(成员)的存在由对象(类)管理
- 部件(成员)不知道对象(类)的存在
一个很好的现实例子是一个人的身体和心脏之间的关系。让我们更详细地研究一下。
组合关系是一种部分-整体关系,其中部分必须构成整个对象的一部分。例如,心脏是一个人身体的一部分。组合中的部分一次只能属于一个对象。心脏是一个人身体的一部分,不能同时成为其他人身体的一部分。
在组合关系中,对象负责部件的存在。通常,这意味着零件会在创建对象时创建,并在销毁对象时销毁。但更广泛地说,它意味着对象会以某种方式管理部件的生命周期,使用对象的用户不需要参与。例如,创建身体时,也会创建心脏。当一个人的身体被摧毁时,他的心脏也会被摧毁。正因为如此,组合有时被称为“死亡关系”。
最后,部分不知道整体的存在。你的心在幸福地运转,却不知道它是更大结构的一部分。我们称之为单向关系,因为身体知道心脏,而心脏不知道身体。
请注意,组合并不限制部件的转移。心脏可以从一个身体移植到另一个身体。然而,即使在移植后,它仍然满足组合的要求(心脏现在由接收者拥有,并且只能是接收者对象的一部分,除非再次转移)。
无处不在的Fraction示例类是一个很好的组合示例:
|
|
这个类有两个数据成员:分子和分母。分子和分母是分数的一部分(包含在其中)。它们一次不能属于多个分数。分子和分母不知道自己是分数的一部分,它们只保存整数。创建分数实例时,会创建分子和分母。当分数实例被销毁时,分子和分母也会被销毁。
虽然对象组合模型是 ”有一个“ 关系(身体有心脏,分数有分母),但可以更准确地说,组合模型是 “一部分” 关系(心脏是身体的一部分,分子是分数的一部分)。组合通常用于建模物理关系,其中一个对象在物理上包含在另一个对象中。
对象组合的部分可以是单数或复数的——例如,心脏是身体里单数的,但身体包含10个手指(可以建模为数组)。
实现组合
组合是在C++中最容易实现的关系类型之一。它们通常创建为具有普通数据成员的结构体或类。因为这些数据成员直接作为结构体/类的一部分存在,所以它们的生命周期绑定到类实例本身的生命周期。
需要进行动态分配或释放的组合可以使用指针数据成员来实现。在这种情况下,组合类应该负责自己进行所有必要的内存管理(而不是类的用户)。
一般来说,如果可以使用组合来设计类,则应该优先使用组合设计类。使用组合设计的类是简单、灵活和健壮的(因为它们很好地进行自我清理)。
更多示例
许多游戏和模拟都有在棋盘、地图或屏幕上移动的生物或对象。所有这些生物/物体的一个共同点是它们都有一个位置。在本例中,我们将创建一个生物类,该生物类使用Point2D类来保存生物的位置。
首先,让我们设计Point2D类。我们的生物将生活在2D世界中,因此Point2D类将有2个维度:X和Y。我们假设世界由离散的方格组成,因此这些维度始终是整数。
Point2D.h:
|
|
注意,因为我们已经在头文件中实现了所有函数(为了保持示例简洁),所以没有Point2D.cpp。
Point2D类有自己的内部成员:位置值x和y是Point2D的一部分,它们的生命周期与对应Point2D实例的生命周期相关联。
现在让我们来设计生物。Creature将具有几个属性:名称(将是字符串)和位置(将是Point2D类)。
Creature.h:
|
|
Creature也有自己的内部成员。名称和位置是它的一部分,并且与所属生物的生命周期密切相关。
最后,main.cpp:
|
|
运行结果:
|
|
组合的变体
尽管大多数组合在创建时直接创建其部分,在销毁时直接销毁其部分,但一些变体稍微改变了这些规则。
例如:
- 组合可能会将某些部分的创建推迟到需要它们时。例如,在用户为字符串分配一些要保存的数据之前,字符串类可能不会创建动态字符数组。
- 组合可以选择直接使用输入的部分,而不是自己创建该部分。
- 组合可以将其部分的销毁委托给其他对象(例如,委托给垃圾收集的例程)。
这里的关键点是,组合负责管理其部分,使用组合的用户不需要管理任何东西。
组合和类成员
当谈到对象组合时,新程序员经常会问:“我应该在什么时候使用类成员,而不是直接实现功能?”例如,与其使用Point2D类来表示Creature的位置,不如将2个整数直接添加到Creature类中,并在Creature类中编写代码来处理定位。然而,让Point2D成为单独的类(同时也是Creature的成员)有许多好处:
- 每个单独的类都相对简单直接,只负责一项任务。这使得每个类都更容易编写和理解,因为它们更专注于自己的职责。例如Point2D只需要关心位置相关的事情。
- 每个类都有完整的功能,因此可以被复用。例如,可以在不同的应用中复用Point2D类。或者creature对象可能需要多个位置(例如它想到达的位置),这时只需要再添加一个Point2D成员即可。
- 父类负责更复杂的协调工作,关注所有成员之间的数据流动。这有助于降低父类本身的复杂度,因为它可以把任务分配给成员,而成员知道如何处理具体细节。例如,当移动Creature时,实际工作会分派给Point类。Point类知道如何设置位置,而Creature类无需知道具体实现细节。
在我们的示例中,Creature不必关心位置点是如何实现的,也不必关心名称是如何存储的,这样的设计是有意义的。Creature的职责不是了解那些细节,而是协调数据流,并确保每个类成员都知道自己应该做什么。具体的实现细节应由各个成员类自己负责。
提示
一个好的经验法则是,应该构建单独的类来完成单个任务。该任务应该是存储和操作某种类型的数据(例如,Point2D,std::string),或者是协调其成员(例如,Creature)。理想情况下不能两者兼而有之。