软件设计 — 类的关系

软件设计 — 类的关系

本文总结软件编程中类与类之间可能存在的关系。

  • 组成
    1. part -of (composition 合成)
    2. has-a (aggregation 聚合)
    3. uses-a (association)
    4. depend-on (dependency)
  • is-a (inherit 继承)

1. composition 合成

复杂对象由简单对象合并而成,也就是说 complex object has-a simpler object

其实 GUI 编程中最普遍的一种关系就是 composition。父容器将其内部的 label, button, image 等组件 (component) 组合在一起。父容器负责子组件的创建和销毁 (生命周期)

这种组合思想最大的好处是: 可重用,灵活,减少耦合

C++ 一般通过类的成员实现 composition。

如何判断是 composition relation?

  1. The part (member) is part of the object (class)
    组件是对象的一部分,即成员

  2. The part (member) can only belong to one object (class) at a time
    每个实例化组件在一个时间点只能属于一个对象

  3. The part (member) has its existence managed by the object (class)
    组件的生命周期由对象负责。如果是指针,则对象负责动态分配和销毁;如果是普通成员,则生命周期和对象相同

  4. The part (member) does not know about the existence of the object (class)
    组件不知道对象的存在,它只需要负责做好自己的工作,实现公共接口供对象调用即可,即关系是单方向的,对象知道它的每个组件,但组件并不知道整体的存在。

2. Aggregation 聚合

不同于 composition 中 对象需要负责它的每个组件的创建和销毁。aggregation relation 中对象不需要对此负责

如何判断是 aggregation relation?

  1. The part (member) is part of the object (class)
  2. The part (member) can belong to more than one object (class) at a time
  3. The part (member) does not have its existence managed by the object (class)
  4. The part (member) does not know about the existence of the object

打个比方:房客和房子的关系,房客有一个房子,但这个房子在某个时间点不一定只属于一个房客,可以由多个房客存在,同时房客也不负责房子的创建和销毁,这是由房东负责的。

这种关系一般通过对象的成员指针或者引用实现,指针指向的对象不需要该对象负责创建和销毁。aggregation usually either takes the objects it is going to point to as constructor parameters, or it begins empty and the subobjects are added later via access functions or operators. Because these parts exist outside of the scope of the class, when the class is destroyed, the pointer or reference member variable will be destroyed, but not deleted, consequently, the parts themselves will still exist.

由于整体和个体的生命周期是独立的,因此需要对个体对象的内存释放非常小心。

3. Association

这个比较难解释。可以认为是对 composition 的进一步解耦。

以司机 (class Driver) 和 汽车 (class Car) 为例

如果以 composition 的关系构建的话,class Driver 会有一个成员变量 Car m_car,表示司机的汽车特征。

可是,进一步思考,从 aggregation 关系出发,我们知道汽车的类型是有限的,如果对每个司机都分配一个成员变量 Car m_car,实在是太浪费了,因为经常是重复的,最好的办法是所有的汽车类型也是实例,class Driver 只要有一个指针 Car *m_car 指向自己的汽车类型实例就好了,这些实例都是已经初始化好了的,不用司机实例创建和销毁

但是,这个方式还有一个弊端,如果从映射的角度出发,我们非得要有 Car *m_car 指针才知道司机的汽车类型吗,如果存在这样一个映射: map: int car_id --> class Car ,我们只需要一个 int 的 ID 就可以知道我们需要的是什么车了。这就好比居民身份证 ID 或者学号一样,学院不需要知道每个学生的信息,它只要知道学生的学号就好了,然后根据学号就可以去另外一张表格查找学生的具体信息。

其实这种思想广泛应用在数据库的表格建立过程中,一张表格的一行表示一个复杂的对象,它也会有对应一个主键 (key word),这样另外一张表格需要使用该表格的对象时,只要用主键的值就好了,其他信息它都不需要。这样的解耦是不是很彻底 ?

Association 设计的关键是主键选择,或者说主键的唯一化。但一般数据库建表时,会有一个默认主键 ID,所以也不用担心

Property Composition Aggregation Association
Relationship type Whole/part Whole/part Otherwise unrelated
Members can belong to multiple classes No Yes Yes
Members existence managed by class Yes No No
Directionality Unidirectional Unidirectional Unidirectional or bidirectional
Relationship verb Part-of Has-a Use-a

4. Dependency

dependency 和 association 非常类似,如果一个对象依赖另外一个对象来实现某个功能时,我们称两者的关系是 dependency

A dependency occurs when one object invokes another object’s functionality in order to accomplish some specific task.

一个典型的例子就是全局实例 std::cout (class std::ostream 类型) ,当我们需要输出信息到控制台时,都会用到这个实例。

Dependency typically are not represented at the class level – the is, the dependent object is not linked as a member. Rather, the dependent object is typically instantiated as needed (like opening a file to write data to), or passed into a function as a parameter.

感觉 dependency 关系一般存在于辅助类或辅助函数中,用于一些功能的实现。

5. inherit 继承

C++ 中继承分单继承和多继承。

这里只给出 C++ 继承关系需要注意的地方

5.1 子类如何调用基类被覆盖的函数

使用 scope::qualifier – BaseClass:: ,如果不加 scope resolution operator (::),那么默认调用子类自己的 identify 函数,会陷入循环调用。

这个有个小技巧,如果父类实现了友元函数,我们不能直接在子类使用父类的友元函数,怎么办? 使用 static_cast<> 将子类转为父类,就可以使用父类的函数了。

5.2 虚函数 (virtual functions) 和多态 (polymorphism)

  1. 如果子类重写 (override) 父类的普通的成员函数,那么父类的指针或者引用指向子类的实例,该指针也只会调用父类的方法,而不是子类的方法
    什么是函数重写 (override) ?
    A derived function is considered a match if it has the same signature (name, parameter types, and whether it is const) and return type as the base version of the function. Such functions are called overrides.
    请注意,返回类型也必须是相同的。

    结果为:

    derived is a Derived
    rBase is a Base
    pBase is a Base
  2. 但是,如果我们给函数加上 virtual 关键词就不一样了
    A virtual function is a special type of function that, when called, resolves to the most derived version of the function that exists between the base and derived class. This capability is known as polymorphism.
    对于虚函数重载,父类的指针或者引用指向子类的实例,该指针会调用子类的方法,这就是 多态 这个术语的由来。
    Note: 不能给构造函数加上 “virtual” 关键词,因为子类的创建依赖于父类,首先创建父类,此时还没有子类,怎么调用子类的函数呢

    结果为:

    derived is a Derived
    rBase is a Derived
    pBase is a Derived

几点值得注意的地方,让代码更加规范:

  1. 加入 override specifier 明确表示子类想要重写基类的虚函数,编译器如果没有找到可以被重写的函数,就会报错,这样就避免运行时错误

    Apply the override specifier to every intended override function you write.

    因为 C++ 中只有函数名称,参数类型,返回类型完全相同的函数才能被覆盖重写

  2. final specifier 表示函数是 虚函数实现的终点了,后面不会再有新的函数重写实现了,这样可以避免继承自己的子类再次重写这个函数。

    这个例子中,class B 重写了 class A 的 getName 函数,可以编译通过。但是 B::getName()final specifier, 意味着任何重写 B::getName() 都被视为错误,因此 C::getName() (有 override 修饰) 试图重写 B::getName() ,编译器出错
    同样,如果 final specifier 修饰 class,那么这个 class 就不能被继承了

  3. covariant return type
    这是一个特别的例子,如果虚函数返回的是 class 指针,子类虚函数返回的 class 指针类型是 父类虚函数返回的 class 指针类型的继承类,那么虽然返回类型不同,仍然被视为重写

    输出结果:

    这里例子中,我们首先调用 d.getThis(), 调用的是 Derived::getThis() ,返回 Derived * 类型,然后该类型用于调用虚函数 Derived::printType()
    如果调用 b->getThis(), 变量 b 是 基类 Base 的指针,指向子类 Derived 对象,由于 Base::getThis() 是虚函数,因此虽然返回类型不同,但仍然调用 Derived::getThis() 虚函数,但千万注意,Derived::getThis() 返回的是 Derived * 类型会退化成 Base * 类型,因此后面调用 Base::printType()

  4. 处理 class inheritance 时,永远记得显式地指明析构函数是 virtual

  5. 如果父类指针指向子类,同时函数也是虚函数,按照 C++ 特性,指针使用的虚函数会调用子类的函数,但如果我们想调用父类的函数怎么办,和前面重写函数调用父类被覆盖的方法类似,用 scope resolution operator 就好了

  6. 抽象类和纯虚函数
    声明虚函数的时候,如果指定为 0 ,则表示纯虚函数,该类也是抽象类

    virtual const char* speak() = 0; // note that speak is a pure virtual function
  7. 所有函数都是纯虚函数的类称为 接口 (Interface),很有用

  8. 使用 dynamic_cast<> 将 基类转为子类,使用之前,请确认转换成功;
    static_cast<> 也可以实现这一点,但它并不做运行时检测,效率更高但也更危险,除非我们可以确认转换一定成功,否则还是用 dynamic_cast<>

    建议:

    • 如果不是 父类转为子类 (downcasting),请使用 static_cast
    • 否则用 dynamic_cast 更合适

    dynamic_cast 比 virtual function 更合适的地点:

    1. 无法修改父类的 virtual function (比如: std 库的父类)
    2. 仅仅只有子类才有这个函数
    3. 给父类添加和子类相同的函数没有意义
  9. 父类的友元函数不能被重写,但我们可以通过虚函数代理实现友元函数的内容,实现间接地友元函数重写。

  10. 直接将子类赋值给父类,而不是通过指针或者引用的方式,那么只会部分拷贝,子类特有的函数和变量都不会传递

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: