[OOP]C++面向对象高效编程 Chapter 2

什么是数据抽象

数据抽象(data abstraction)在忽略类对象间存在差异的同时,展现了对用户而言最重要的特性。的确,抽象应该对终端用户隐藏无关紧要的细节,避免暴露有可能分散用户注意力或与使用环境毫不相干的细节。

1 接口和实现的分离

接口的含义

类对象的接口支持外部视图,接口就是用户观察的对象视图,以及用户可以用接口做什么(也包括接口对用户做什么)。

当我们设计接口时,应最大程度地满足用户的要求,这些用户也称为客户(client)。所谓类的客户,就是使用类且不知道(或不用关心)类内部运作的人。客户可创建类的对象,并通过接口对其进行操作。

实现的含义

接口告诉客户可以做什么,实现则负责如何做,所有的工作都在实现中完成。客户无需了解类如何实现接口所提供的操作。因此,实现用于支持由对象表现的接口。

2 对象接口的重要性

数据抽象的目的是,提供清晰和充足的接口,在方便且受控的模式下允许用户访问底层实现。

3 保护实现

如果实现在生存期内无法保证自身的完整性,这样的实现则毫无用处。

接口(速度表、ATM、电源插座)都由实现支持,而且该实现由对应的接口来保护(即接口提供一个清晰且定义明确的方法访问实现)。换言之,实现以特定方式工作,并跟踪自身的状态。另外,实现假设它的状态仅能通过接口更改,如果违反此前提条件(即不知何故,实现的状态被直接从外部更改,并未通过提供的接口更改),则无法保证实现进行正确地操作。

数据抽象引出了相关的概念:数据封装(data encapsulation)。只要存在由实现支持的带接口的对象,就一定存在实现隐藏(implementation hiding)(也称为信息隐藏)。有些信息对实现而言相当重要,但是对使用接口的用户而言却毫无价值,这些信息将被隐藏在实现中。实现由接口封装,而且接口是访问该实现的唯一合法途径。

被封装的数据对于对象的实现极其重要。进一步而言,实现必须维护被封装信息的完整(或正确的状态)。

4 数据封装的优点

数据被封装后,客户无法直接访问,更不能修改,只有接口函数才可访问和修改封装的信息。进一步而言,使用接口的用户完全不知道描述该接口的函数如何使用封装信息,而且对象(或类)的用户对此也毫无兴趣。

数据封装的另一个优点是实现独立(implementation independence)。由于类的用户无法查看封装的数据(或信息),他们甚至不会注意到封装数据的存在。因此,改动封装内的数据不会(也不该)影响用户所见的接口。

5 接口、实现和数据封装之间的关系

  • 接口是任何类(和它的对象)客户的视图;
  • 接口由封装的实现支持;
  • 改变类的实现(支持接口)不应该影响该类客户所见的接口;
  • 封装的实现能让实现者修改实现但不影响接口。即客户使用的接口与支持接口的实现彼此独立;

6 确定封装的内容

  • 如果某项对于用户理解类毫无帮助,封装该项,即从接口中移除该项根本不会减少类的效用;
  • 如果某项包含敏感数据(商业秘密、专利信息、个人信息等),为了不让用户直接访问,封装该项;
  • 如果某些项有潜在的危险(激光束、X射线、微波等),并且要求用户掌握特殊技能(普通用户不具备)才能操作,则封装该项;
  • 如果类为了自我管理而使用某些元素,且对接口意义不大,应封装这些元素;
  • 如果某些项倾向于在将来进行改动(为了使用更新的技术或者让其更快或更安全),必须从类的接口中移除,封装这些项。

7 抽象数据类型

抽象数据类型是由程序员定义的新类型,附带一组操控新类型的操作。定义新抽象类型将直接应用数据抽象的概念。抽象数据类型也称为程序员定义类型(programmer defined type)。

8 对象是重点

在面向对象编程中,我们总是使用对象。要牢记:无论何时我们讨论调用函数或发送消息时,都必须涉及一个对象。我们通过对象调用函数(或发送消息给对象),在没有对象接收消息的情况下,不能发送消息。必须有的放矢,对象即是目标。

9 什么是多线程安全类

一个进程内的两个线程可以使用相同的对象。当一个线程调用一个成员函数,在成员函数内部完成执行之前,如果(操作系统)调度(schedule)另一个线程运行,且该线程也通过相同的对象调用相同的成员函数,则对象必须保证自身完整和运行良好。如果对象不能做到这一点,这样的类就不是线程安全(thread-safe)的。

10 确保抽象的可靠性——类不变式和断言

10.1 类不变式

每个类都会在对象中包含一些恒为真的条件,无论对象调用任何成员函数,这些条件都必须为真。这样的条件称为类不变式(class invariant)。

10.2 前置条件和后置条件

成员函数可能会包含其他条件,在执行代码前必须保证这些条件为真。这些在操作开始被调用之前必须为真的条件,称为前置条件(precondition)

在C和C++中,断言已经使用很长一段时间。所有的C和C++编译器都支持assert宏。该宏接受一个表达式,而且必须判断表达式的真假。倘若表达式判断为真,则继续执行;倘若表达式为假,则程序停止,并显示错误消息表明断言失败。

10.3 高效使用断言

高效使用断言可实现更可靠的程序。当条件不成立时,断言至少保证程序不会继续执行。但是发生断言失败,就不可能再复原。要解决这个问题,需要用到 C++支持的真正的异常管理工具。

11 统一建模语言(UML)

11.1 类和对象的表示

类用矩形表示。类名通常用粗体表示。属性在类名下的第二栏中列出,操作在类名下的第三栏中列出。在高级概述图中,第二栏和第三栏可以省略,只在矩形中显示类名即可。

在类名的上方可以规定类的衍型(stereotype)。衍型表示它是何种类型的类,如异常类、控制类、接口类等。衍型包含在一对双尖括号中。

抽象类的名称用斜体表示,抽象操作也用斜体表示。

对象用矩形表示,矩形中的对象名和类名带下划线。

顶格中以对象名:类名的形式显示。匿名对象可以省略对象名。

11.2 UML中类的关系

11.2.1 关联关系

关联表示对象与不同类之间的结构关系(structual relationship),大多数关联都是二元关系(binary relation)。类之间的多重关联(multiple association)和类本身的自关联(self association)都是合法的。

每个关联的末端就是角色。每个角色都有一个名称,说明其他的类如何看待这个类。Company将Person看做成“雇员”。类似地,Person将Company看成“雇主”。角色名必须唯一,它比关联名更重要。

每个角色都说明了类的多重性(multiplicity)。例如,Person可以为许多公司工作(即人与许多公司相关联)。这说明角色的多重性。符号 * 表明“许多”(对象的无限数目,其表示为 0..*)。多重性也可以是一个数字(1或5等等),或者是一个范围(1..5)。

关联末端的箭头表明关联的导航性。

一个类还可以与本身形成关联,即成为关联类(association class)。在关联线用虚线引出的类,即是关联类。

11.2.2 包含关系(has-a)

聚集

聚集(aggregation)是一种特殊形式的关联。这种情况下,部分(即整体所包含的部分)的生存期不再取决于整体的生存期。

通过一个空菱形连接的类为聚集。不能在线的两端都绘制菱形。当类之间没有生存期依赖时,该表示法用于表示常见的按引用聚集。Orchestra(管弦乐队)是Performer(演奏者)的全体演出者。如果将表示聚集的空菱形填充,则表示组合(composition)——聚集的一种加强的形式。

组合

这是一种聚集形式,有很强的生存期,且部分和整体之间的所有权依赖关系也很强。聚集(容器)的多重性不能超过1个(无共享)。组合的对象一起被创建,一起被销毁。

当多重性(基数)大于 1 时,可以在创建聚集本身后再创建部分(part),除非在聚集被销毁前,显式移除部分,否则部分会和聚集一起被销毁。一架 AirPlane 有多个 Engine和多个Seat等,而且在AirPlane类对象的生存期内,可以添加或移除Seat。当AirPlane类对象被销毁后,它所包含的所有对象都会被销毁,除非它们已经从AirPlane类对象中移除(例如,座椅可能被移除,复用于另一架飞机中)。

OR关联

在某些情况下,一个类可以参与两个关联,但是每个对象一次只能参与一个关联。

11.2.3 泛化关系(is-a)

泛化关系用于表示继承,意味着从基类到派生类的一般——特殊关系,图中的箭头必须为空心


[OOP]C++面向对象高效编程 Chapter 2
https://erlsrnby04.github.io/2024/10/28/OOP-C-面向对象高效编程-Chapter-2/
作者
ErlsrnBy04
发布于
2024年10月28日
许可协议