设计与对象:理解“有一个(Has-a)”与“是一个(Is-a)”关系 参考:Professional C++ (English Edition) 5th Edition

设计与对象:理解“有一个(Has-a)”与“是一个(Is-a)”关系

我将讨论对象之间的不同关系类型,包括程序员在构建面向对象程序时常常陷入的陷阱。在思考过程式编程或面向对象编程时,最重要的一点是要记住它们只是不同的方式来理解程序中发生的事情。程序员往往在充分理解什么是对象之前就陷入了面向对象编程的语法和术语中。

在面向对象编程(OOP)中,类与类之间的关系是一个基础且关键的概念。主要有两种类型的类关系:一种是“有一个(Has-a)”关系,另一种是“是一个(Is-a)”关系。这两种关系模型在日常应用开发和设计模式中有着广泛的应用。本文将深入解析这两种关系,以及如何在实际编程中应用它们。

“有一个(Has-a)”关系

基本概念

在“有一个(Has-a)”关系中,可以将一个类视为另一个类的组成部分。例如,考虑一个动物园(Zoo)和猴子(Monkey)的关系。动物园拥有猴子,或者说,动物园包含猴子。在这种情况下,我们可以说这两者之间存在“有一个(Has-a)”的关系。

聚合与组合

“有一个(Has-a)”关系有两个子类别:

  1. 聚合(Aggregation): 聚合对象(组件)在聚合体被销毁时仍然可以继续存在。例如,动物园破产并被销毁,但里面的动物不会被销毁,而是会被转移到另一个动物园。
  2. 组合(Composition): 如果由其他对象组成的对象被销毁,那么这些其他对象也会被销毁。例如,包含按钮的窗口对象被销毁,那么按钮对象也会被销毁。

image-20231024152314973

“是一个(Is-a)”关系(继承)

基本概念

“是一个(Is-a)”关系是面向对象编程中的基础概念,也被称为派生、子类化、扩展和继承。例如,猴子是动物,长颈鹿是动物,这些都是“是一个(Is-a)”的关系。

技巧与应用

  1. 添加功能(Adding Functionality): 派生类可以通过添加额外的功能来增强其父类。例如,猴子是一种动物,但它有额外的swingFromTrees()方法。
  2. 替换功能(Replacing Functionality): 派生类可以完全替换或重写其父类的某个方法。
  3. 添加属性(Adding Properties): 派生类还可以添加新的属性。
  4. 替换属性(Replacing Properties): C++提供了一种类似于方法重写的属性重写方式。

多态(Polymorphism)

多态性是指遵循一组标准属性和方法的对象可以互换使用的概念。类定义就像对象和与之交互的代码之间的合同。根据定义,任何Monkey对象都必须支持Monkey类的属性和方法。这个概念也适用于基类。因为所有的猴子都是动物,所有的Monkey对象也支持Animal类的属性和方法。多态性是面向对象编程中美妙的一部分,因为它真正利用了继承所提供的优势。在动物园模拟中,您可以通过编程方式循环遍历动物园中的所有动物,并让每个动物移动一次。因为所有的动物都是Animal类的成员,它们都知道如何移动。一些动物已经重写了移动方法,但这就是最好的部分-您的代码只是告诉每个动物移动,而不知道或关心它是什么类型的动物。每个动物都按照自己所知的方式移动。两者之间的界限

在现实世界中,很容易对对象之间的“有一个(Has-a)”和“是一个(Is-a)”关系进行分类。然而,在代码中,这些界限有时并不那么清晰。

案例分析:AssociativeArray 与 MultiAssociativeArray

背景

假设我们有一个名为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);
    // ... 其他方法
};

image-20231024152933978

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);
    // ... 其他方法
};

image-20231024152946092

关系类型优点缺点
Is-a(继承)- 能够复用AssociativeArray的所有功能和代码
- 可以用AssociativeArray对象的地方也能使用MultiAssociativeArray对象
- get()方法需要重写以返回一个特定的值,这可能会引入复杂性
- 如果AssociativeArray类的实现发生变化,可能需要更改MultiAssociativeArray
Has-a(组合)- 更灵活,因为可以更容易地修改或扩展MultiAssociativeArray的功能
- 不必担心AssociativeArray的任何功能或方法“渗透”到MultiAssociativeArray
- 可能需要编写更多的代码来调用内部AssociativeArray对象的方法

Liskov替换原则(Liskov Substitution Principle,LSP)

LSP建议,派生类对象应该能够替换其基类对象,而不改变程序的正确性。在这个案例中,由于MultiAssociativeArrayinsert()方法与AssociativeArray的行为不同(不会删除具有相同键的早期值),因此更倾向于Has-a关系。

最终推荐

基于上述分析,推荐使用Has-a关系。这样,MultiAssociativeArray可以有自己独立的接口和实现,同时还可以灵活地应对未来需求的变化。

结论

理解和应用“有一个(Has-a)”和“是一个(Is-a)”关系是面向对象编程的关键。选择哪一种关系取决于多种因素,包括设计需求、可维护性和未来扩展性。经验表明,如果两者之间有选择,通常更推荐使用“有一个(Has-a)”关系。

参考:Professional C++ (English Edition) 5th Edition by Marc Gregoire

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值