关联(Association) && 聚合(Aggregation) && 组合(Composition)

组合

概述

在现实生活中,复杂的对象通常是由小的,简单的组成,从简单对象构建复杂对象的过程称为对象组合

例如

  • 汽车是用金属框架,发动机,轮胎,变速器和其他大量零件制造而成的
  • 个人电脑由CPU,主板,内存等组成
  • 即便是你也是由较小部分组成:头,身体,腿,手

从广义上讲,两个对象存在关系构成了对象组合模型

  • 汽车有变速箱,你的电脑有CPU,你自己有心脏
  • 复杂对象有时被称为整体或父对象,简单的对象通常被称为零件,子零件或组件

在C++中,你已经看到结构和类可以具体各种类型的数据成员(例如基础类型和其他类)

当我们使用数据成员构建类时,实际上是从较简单的部分构造复杂对象,即对象组合,因此,有时将结构和类称为复合类型

对象组合在C++上下文中很有用,因为它使我们可以通过组合更简单,更易于管理的创建复杂的类

这降低了复杂,并且使我们能够更快地编写代码并减少错误,因此我们可以重用已经编写,测试和验证过的有效代码

对象组合的类型

对象组合有两种基本的子类型

  • 组合(composition)
  • 聚合(aggregation)

关于术语的注释:术语“组合”通常用于指组合和聚合,而不仅指组合

在本篇文章中,当我们提及两者时,将术语“对象组合”,而当具体提及组合,将使用术语组合(composition)

组合

为了符合组合条件,对象和零件必须具有以下关系

  • 该部件(成员)是对象(类)的一部分
  • 该部件(成员)一次只能属于一个对象(类)
  • 该部件(成员)的存在由对象(类)管理
  • 该部件(成员)不知道对象(类)的存在

一个真实的组合例子就是一个人的身体和心脏的关系

组合的构成是部件 - 整体的关联,部件是整体的一部分

  • 例如,心脏是人的身体的一部分。组合中的部件一次只能是一个对象的一部分
  • 属于一个人的身体的心脏不能同时属于另一个人的身体

在组合构建中,对象负责部件的存亡

  • 意味着,部件在对象创建的时候创建,并在销毁对象时销毁
  • 从更广的范围来说,这意味着,对象不需要用户参与管理部件的生命周期
  • 例如,当创建一个身体时,心脏也被创建。当一个人的身体被破坏时,他们的心脏也被破坏。因此,这就构成了“死亡关系”

部件不知道整体的存在

  • 你的心脏幸福地运转着,却没有意识到它是更大结构的一部分
  • 我们称其为单向关系,因此身体了解心脏,但反之未然

部件的可移植说明

  • 心脏可以从一个身体移植到另外一个身体
  • 移植后仍然符合组合关系的要求(心脏现已由接受者拥有,除非再次移植,否则它只能是接受者的一部分)

例子

class Fraction
{
    private:
        int m_numerator;
        int m_denominator;

    public:
        Fraction(int numerator=0, int denominator=1):
            m_numerator{ numerator }, m_denominator{ denominator }
        {
            // We put reduce() in the constructor to ensure any fractions we make get reduced!
            // Since all of the overloaded operators create new Fractions, we can guarantee this will get called here
            reduce();
        }
};
  • 此类具有两个数据成员:分子和分母。分子和分母是分数的一部件(包含在其中)
  • 它们一次不能属于一个以上分数。分子和分母不知道它们是分数的部件,它们知道表示某个整数
  • 创建分数实例的时候,同时创建分子和分母两个成员变量。当分数被销毁的时候,分子和分母的成员变量也被销毁

更多例子

#include <iostream>
#include <string.h>

class Point2D
{
    private:
        int m_x;
        int m_y;

    public:
        // a default constructof
        Point2D(): m_x{0}, m_y{0}
        {

        }

        // a specific constructor
        Point2D(int x, int y): m_x{x}, m_y{y}
        {

        }

        // an overloaded output operator
        friend std::ostream& operator <<(std::ostream& out, const Point2D &point)
        {
            out << '(' << point.m_x << ", " << point.m_y << ')';
            return out;
        }

        // Access functions
        void setPoint(int x, int y)
        {
            m_x = x;
            m_y = y;
        }
};


class Creature
{
    private:
        std::string m_name;
        Point2D m_location;
    
    public:
        Creature(const std::string &name, const Point2D &location)
            :m_name{name}, m_location{location}
        {
            
        }

        friend std::ostream& operator<<(std::ostream& out, const Creature &creature)
        {
            out << creature.m_name << " is at " << creature.m_location;
            return out;
        }

        void moveTo(int x, int y)
        {
            m_location.setPoint(x, y);
        }
};

int main() {

    std::cout << "Enter a name for your creature.";
    std::string name;
    std::cin >> name;
    Creature creature{name, {4, 7}};

    while (true) {
        // print the creature's name and location
        std::cout << creature << '\n';

        std::cout << "Enter new X location for creature (-1 to quit): ";
        int x{0};
        std::cin >> x;
        if (x == -1) {
            break;
        }
        
        std::cout << "Enter new Y location for creature (-1 to quit):";
        int y{0};
        std::cin >> y;
        if (y == -1) {
            break;
        }
        creature.moveTo(x, y);
    }

    return 0;
}

组合的变体

尽管大多数组合模型中,部分随着整体创建而创建,部分随整体销毁而销毁。但是还有一些不一样的情况

例如

  • 在组合关系中,可能会延迟某些部分的创建,直到它们需要为止。例如,用户为字符串分配一些要保留的数据之前,字符串类可能不会创建动态字符数组
  • 在组合关系中,可能会选择使用已提供给它的零件作为输入,而不是自己创建零件
  • 在组合关系中,可能会将其部分的销毁要求委托给其他对象。如,垃圾回收例程

这里的关键点是,组合中整体管理部分,而组合的用户则不需要进行任何管理

组合和子类

当涉及对象组合时新程序员经常会问一个问题:“我什么时候应该使用子类而不是直接实现功能?”

例如,我们可以不使用Point2D类来实现Creature的位置,而可以只向Creature类添加2个整数,并在Creature类中编写代码来处理位置

将Point2D设为自己的类有很多好处:

  • 每个对象可以保持简单明了,专注于出色地完成一项任务,由于它们职责更分明,因此使这些类更容易编写和理解
    • 例如,Point2D只关注与点有关的时间,这使理解和编写代码更加简单
  • 每个子类可以独立的,这使得它们可以重用
    • 例如,我们可以在完全不同的应用程序中重用Point2D类
    • 或者,我们需要另外一个点作为试图要到达的目的地,那么可以简单添加另一个Point2D成员变量
  • 父类可以让子类完成大部分的工作,而自己专注协调子类之间的数据流。这有助于降低父对象的整体复杂性,因为它可以将任务委派给已经知道如何执行这些任务的子对象
    • 例如,当我们移动Creature时,它将任务委派给Point类,该类已经了解如何设置点
    • 因此,Creature类不必担心如何实现这些事情

一个好的经验法则是,应该构建单个类对应单个任务

在我们的示例中, 很明显Creature不必担心Point的实现方式或名称的存储方式

  • Creature的工作不需要知道哪些私密的细节
  • Creature的工作是担心如何协调数据流,并确保每个子类都知道应该做什么。接下来有各个子类担心它们怎么做

聚合

概述

要符合聚合条件,整个对象及其各个部件必须具有以下关系

  • 该部件(成员)是对象(类)的一部分
  • 该部件 (成员) 一次可以属于多个对象(类)
  • 该部件 (成员)的存在不由对象(类)管理
  • 该部件(成员)不知道对象(类)的存在

像组合一样,聚合仍然是部件与整体的关系,其中部件包含在整体中,并且是单向关系,但是与组合不同,部件一次可以属于一个以上的对象,而整个对象对部件的存在和生命周期不负责

创建聚合时,聚合不负责创建部件,销毁聚合时,聚合不负责销毁部件

例如,考虑一个人与其家庭住址之间的关系

  • 在此示例中,为简单起见,我们将说每个人都有一个地址。但是,该地址一次可以属于一个以上的人:如你和你的室友或其他重要的人
  • 但是,该地址不是由该人管理的-该地址可能在此人到达之前就已存在,而在该人离开后仍然存在
  • 另外,一个人知道他们住的地方,但地址不知道他们住的地方
  • 因为这是一个聚合关系

另外,考虑一下汽车和引擎

  • 汽车发动机是汽车的一部分,尽管引擎属于汽车,但它也可以属于其他事物,例如拥有汽车的人
  • 汽车不负责发动机的制造和毁坏
  • 虽然汽车知道它有引擎,但是引擎却不知道它是汽车的一部分

在对物理对象建模时,使用“破坏”一词可能会有些麻烦

  • 有人可能会争辩说“如果流星从天上掉下来砸碎了汽车,汽车零件也不会全部被毁吗?”当然是,但这是流星的错
  • 重要的是,汽车对零件的损坏不承担任何责任(但可能会受外力的影响)

我们可以说聚合模型“有”关系(一个部分有老师,汽车有引擎)。与组合类似,集合的各个部件可以单独的或相互调用

聚合案例

因为聚合与组合相似,因为它们都是部件 - 整体关系,因此它们的实现几乎完全相同,它们之间的差异主要是语义上的

在组合中,我们通常使用普通成员变量(或有组合类处理分配和释放过程的指针)将零件添加到组合中

在聚合中,我们还将部件添加为成员变量,但是,这些成员变量通常是引用或指针,用于指向已在类范围之外创建的对象。因此,聚合通常要么指向的对象用作构造函数参数,要么开始为空,然后再通过访问函数或运算符添加子对象

因为这些部分存在于类范围之外,所以当销毁该类时,指针或引用成员变量将销毁(但不会删除)。因此,部件本身仍将存在

#include <iostream>
#include <string>

class Teacher
{
    private:
        std::string m_name{};
    
    public:
        Teacher(const std::string& name): m_name{name}
        {

        }

        const std::string& getName() const
        {
            return m_name;
        }
};

class Department
{
    private:
        const Teacher& m_teacher;   // This dept holds only one teacher of simplicity, but it could hold many teachers
    public:
        Department(const Teacher& teacher): m_teacher{teacher}
        {

        }
};

int main() {

    // Create a teacher outside the scope of the Department
    Teacher bob {"Bob"};    // create a teacher

    {
        // Create a department and use the consturctos partameter to pass
        // the teacher to it.
        Department department {bob};
    }   // department goes out of scope here and is destoryed

    // bob still exists here, but the department doesn't
    std::cout << bob.getName() << " still exists! \n";
    return 0;
}
  • 首先,该部门只能容纳一位老师。其次,老师不会知道他们所属的部门
  • 所以在本例中,bob是独立于department创建的,然后传递给department的构造函数
  • 当department 被销毁的时,m_teacher引用被销毁。但是是教师本身并没有被销毁,因此它仍然存在,直到后来在main()被独立销毁

为建模选择正确的关系

尽管上面的例子中教师不知道他们为那么部门工作似乎有些愚蠢,但在给定程序的背景下这可能完全没问题

在确定要实现哪种关系时,请实现最简单的满足你的需求,而不是看起来最适合现实生活的关系

例如,如果你正在编写车身修理厂模拟器,则可能希望将汽车和引擎作为聚合来实现,因此可以将引擎卸下并放在架子上以备后用

但是,如果你正在编写赛车模拟游戏,则可能希望将汽车和引擎作为组合来实现,因为在这种情况下,引擎永远不会存在于汽车外部

关联

概述

要符合关联条件,一个对象与另一个对象必须具有以下关系

  • 关联的对象(成员)与对象(类)无关
  • 关联的对象(成员)一次可以属于多个对象(类)
  • 关联的对象(成员)没有由该成员(类)管理其存在
  • 关联的对象(成员)可能知道或可能不知道对象(类)的存在

与组合和聚合的部分是构成整体的一部分不同,在关联中,关联的对象与该对象无关

就像聚合一样,关联的对象可以同时属于多个对象,并且不受这些对象的管理

但是,与聚合(关系始终是单向的)不同的是,在关联中,关系可以是单向的也可以是双向的(两个对象相互了解)

  • 医生与患者之间的关系就是一个很好的例子
  • 医生显然与患者有关系,但从概念上讲,这个不是部分/整体(对象组合)的关系
  • 一个医生一天可以看很多病人,一个病人可以看很多医生(也许想要第二个医生意见,或者他们正在拜访不同类型的医生)
  • 该对象的寿命都没有和另外一个有关系
  • 我们可以说关联模型的是“用户-单个”关系。医生“使用”病人(以赚取收入)。患者使用医生(出于他们所需的任何健康目的)

案例

#include <cstdint>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

// Since Doctor and Patient have a circular dependency, we're going to farward declare Patient
class Patient;

class Doctor
{
    private:
        string m_name{};
        vector<reference_wrapper<const Patient>> m_patient{};

    public:
        Doctor(const string& name): m_name{name}
        {

        }

        void addPatient(Patient& patient);

        // We'll implement this function below Patient since we need Patient to be defined at that point
        friend ostream& operator<<(ostream &out, const Doctor &doctor);

        const string& getName() const 
        {
            return m_name;
        }
};

class Patient
{
    private:
        string m_name{};
        vector<reference_wrapper<const Doctor>> m_doctor{}; // so that we can use it here

        // We're going to make private because we don't want the public to use it
        // They should use Doctor::addPatient() instead, which is publiy exposed
        void addDoctor(const Doctor& doctor) 
        {
            m_doctor.push_back(doctor);
        }

    public:
        Patient(const string& name): m_name {name}
        {

        }

        // We'll friend Doctor::addPatient() so it can access the private function Patient::addDoctor()
        friend ostream& operator << (ostream &out, const Patient &patient);

        const string& getName() const 
        {
            return m_name;
        }

        // We'll friend Doctor::addPatient() so it can access the private function Patient::addDoctor()
        friend void Doctor::addPatient(Patient& patient);
};

void Doctor::addPatient(Patient& patient)
{
    // Our doctor will add this patient
    m_patient.push_back(patient);

    // and the patient will also add this doctor
    patient.addDoctor(*this);
}

ostream& operator<<(ostream &out, const Doctor &doctor)
{
    if (doctor.m_patient.empty()) {
        out << doctor.m_name << " has no patients right now";
        return out;
    }

    out << doctor.m_name << " is seeing patients: ";
    for (const auto& patient: doctor.m_patient) {
        out << patient.get().getName() << ' ';
    }

    return out;
}

ostream& operator<<(ostream &out, const Patient &patient)
{
    if (patient.m_doctor.empty()) {
        out << patient.getName() << " has no doctors right now";
        return out;
    }

    out << patient.m_name << " is seeding doctors: ";
    for (const auto& doctor: patient.m_doctor) {
        out << doctor.get().getName() << ' ';
    }

    return out;
}

int main() {

    // Create a Patient outside the scope of the Doctor
    Patient dave {"Dave"};
    Patient frank {"Frank"};
    Patient betsy {"Betsy"};

    Doctor james {"James"};
    Doctor scott {"Scott"};

    james.addPatient(dave);

    scott.addPatient(dave);
    scott.addPatient(betsy);

    cout << james << '\n';
	cout << scott << '\n';
	cout << dave << '\n';
	cout << frank << '\n';
	cout << betsy << '\n';
    return 0;
}

因为关联是一种广泛的关系,所以可以用许多不同的方式来实现它们。但是,大多数情况下,关联是使用指针实现的,其中对象指向关联的对象

在这个示例中,我们将实现双向“医生/患者”关系,因为让医生知道他们的患者是有意义的,反之亦然

通常,如果可以使用单向关联,则应避免使用双向关联,因为它们会增加复杂性,并且往往更容易编写而不会错误

反身关联

有时对象可能与相同类型的其他对象有关系,这被称为反身关联

反身关联的一个很好例子是大学课程与其先决条件(也就是大学课程)之间的关系

考虑简化的情况,其中一门课程只能有一个前提条件。我们可以做这样的是事情

include <string>
   class Course
   {
       private:
           std::string m_name;
           const Course *m_prerequisite;

       public:
           Course(const std::string &name, const Course *prerequisite = nullptr): m_name{name}, m_prerequisite{prerequisite}
           {
           }
   };
  • 这可能会导致关联链(课程具有先决条件等)

关联可以是间接的

#include <iostream>
#include <string>
using namespace std;

class Car
{
    private:
        string m_name;
        int m_id;
    
    public:
        Car(const string& name, int id): m_name{name}, m_id{id}
        {

        }

        const string& getName() const
        {
            return m_name;
        }

        int getId() const
        {
            return m_id;
        }
};

// Our CarLot is essentially just a static array of Cars and a lookup function to retrieven them
// Because it's static, we don't need to allocate an object of type CarLot to use it
class CarLot
{
    private:
        static Car s_carLot[4];
    
    public:
        CarLot() = delete;  // Ensure we don't try to create a CarLot

        static Car* getCar(int id)
        {
            for (int count{0}; count < 4; ++count) {
                if (s_carLot[count].getId() == id) {
                    return &(s_carLot[count]);
                }
            }
            
            return nullptr;
        }
};

Car CarLot::s_carLot[4]{{"Prius", 4}, {"Corolla", 17}, {"Accord", 84}, {"Matrix", 62}};

class Driver
{
    private:
        string m_name;
        int m_carId;        // we're associated with the Car by ID rather than pointer

    public:
        Driver(const string& name, int carId): m_name{name}, m_carId{carId}
        {

        }

        const string& getName() const 
        {
            return m_name;
        }

        int getCarId() const
        {
            return m_carId;
        }
};

int main() {

    Driver d {"Franz", 17};                     // Franz is driving the car with ID 17

    Car *car {CarLot::getCar(d.getCarId())};    // Get that car from the car lot

    if (car) {
        cout << d.getName() << " is driving a " << car->getName() << '\n';
    } else {
        cout << d.getName() << " couldn't find his car \n";
    }

    return 0;
}

在上面的示例中,我们有一个CarLot来存放我们的汽车。需要汽车的驾驶员没有指向他的汽车的指针-相反,他具有汽车ID,我们可以使用该ID在需要时从CarLot中获取汽车

在这个特定示例中,以这种方式执行操作有点愚蠢,因为将Car将退出CarLot要求的查找效率很低(将两者连接的指针要快得多)

但是,通过唯一的ID而不是指针来引用事物也具有优势

  • 例如,你可以引用当前不在内存中的内容(也许它们在文件或数据中,并且可以按需加载)
  • 同样,指针可以占用4或8个字节。如果空间有限,并且唯一对象的数量很少,则用8位或16位整数引用它们可以节省大量内存

总结

组合和聚合的总结

组合

  • 通常使用普通成员变量
  • 如果类本身处理对象分配/取消,则可以使用指针成语
  • 负责部件的创建/销毁

聚合

  • 通常使用指向或引用位于聚合类范围之外的对象的指针或引用成员
  • 不负责创建/销毁部件

值得注意的是,组合和聚合的概念不是相互排斥的,可以在同一类中自由混合。完全可以编写一个类来负责创建/销毁某些部分,而不是其他

例如,我们的部门类可以有一个名字和一个老师类

  • 该名称可以按组合添加到部门中,并随部门一起创建和销毁
  • 另一方面,将通过汇总将教师添加到部门,并独立创建/销毁该老师

尽管聚合可能非常有用,但它们也可能更加危险,因为聚合无法处理其部分的重新分配,解除分配留给外部人员完成。如果外部方不再指向废弃部分的指针或引用,或者只是忘记进行清理(假设该类将进行清理),则内存将被泄漏

出于这个原因,应该优先使用组合而不是聚合

例子

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Teacher
{
    private:
        std::string m_name{};
    
    public:
        Teacher(const std::string& name): m_name{name}
        {

        }

        const std::string& getName() const
        {
            return m_name;
        }
};

class Department
{
    private:
        vector<Teacher> m_listOfTeachers;
    public:
        Department()
        {

        }

        void add(const Teacher& teacher)
        {
            m_listOfTeachers.push_back(teacher);
        }

        friend std::ostream& operator<<(std::ostream& out, const Department &department)
        {
            out << "Department: ";
            for (auto& teac : department.m_listOfTeachers) {
                out << teac.getName() << ' ';
            }
            return out;
        }
};

int main() {

    // Create a teacher outside the scope of the Department
    Teacher t1 {"Bob"};
    Teacher t2 {"Frank"};
    Teacher t3 {"Beth"};

    {
        // Create a department and add some Teachers to it
        Department department{};    // create an empty Department

        department.add(t1);
        department.add(t2);
        department.add(t3);

        cout << department;
    }   // department goes out of scope here and is destoryed

    cout << t1.getName() << " still exists! \n";
    cout << t2.getName() << " still exists! \n";
    cout << t3.getName() << " still exists! \n";
    return 0;
}

组合 vs 聚合 vs 关联 总结

属性组合聚合关联
关系类型整体/部分整体/部分没有特定关系
成员属于多个类别
类管理成员生命周期
方向性单向单向单向或双向
关系动词Part-ofHas-aUses-a
  • 31
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值