- 面向对象分析与设计
- 孙学波 卢圣凯等编著
- 6844字
- 2025-02-22 20:01:21
1.1.2 对象模型的构成要素
对象模型是面向对象方法的逻辑基础。从概念上来说,对象模型包括抽象、封装、模块化、层次结构、类型、并发和持久七个基本构成要素,其中抽象、封装、模块化、层次结构为主要要素,类型、并发和持久为次要要素。其中主要要素是指模型必须包含的要素,缺少任何一个主要要素,这个模型都不再被称为是面向对象的模型。次要要素是指这些要素是对象模型的有用组成部分,但不是本质要素。
正确地理解这个概念框架,对于使用面向对象方法开发应用系统具有非常重要的理论意义和现实意义。反之,即使使用了面向对象程序设计语言,但编写出来的程序可能仍然不具备面向对象的特征,很可能与传统的结构化应用程序没有太大区别。更重要的是,使用面向对象技术的主要目的是为了更好地把握问题的复杂性。
1.抽象
抽象(Abstract)是人们分析复杂事物时常用的基本思维方式,也是人正确认识客观事物、解决问题的重要方法。
抽象也可以看成是对客观事物的一种简单的概述性描述,通常仅关注客观事物的重要细节,而忽略事物的非本质特征的细节或枝节。对于一个概念,只有当它可以独立于最终使用和实现它的机制来描述、理解和分析时,我们才说这个概念是抽象的。
例如,在数据结构中,线性表是对具有线性关系的数据集合及其处理的一个抽象描述。这个概念抓住了数据元素之间的线性关系及其处理算法等这一本质特征,而忽略了其元素的数据类型、存储结构和算法的实现等具体特征。这个抽象可以抓住问题的本质特征而忽略问题具体的细枝末节,从而使我们可以在更高的层次上讨论要解决的问题。
也可以说,抽象是对客观事物所具有的基本特征的概念性描述,通过抽象可以将一种事物与其他事物严格区分开。抽象也是一种分析问题和解决问题的基本方法。
在面向对象方法中,对象、属性、方法、类、责任、职责和接口等都是运用抽象思维得出的抽象概念,正是这些概念构成了面向对象方法的基本词汇。
面向对象方法要求建模人员能够正确地运用抽象这一基本的思维方式,正确认识问题域中所面临的问题及其解决方法。从给定问题域中分析出一组正确的抽象,则是面向对象分析与设计的核心问题。
面向对象分析的实质,就是从问题域中抽象出对实现系统目标有意义的对象。这些对象可能包括问题域中的实体对象、通用操作对象、业务逻辑对象、偶然对象等,因此,面向对象分析的过程实质就是一系列的抽象过程。
作为一种最基本的思维方式,抽象的运用范围不仅仅在于发现对象,而是更广泛地存在于软件开发过程中所涉及的每个不同的领域。
确定了一个对象的属性和行为,仅仅给出了这个对象在某种意义上的一个抽象,还需要进一步给出其具体实现。一个对象通常可以用多种方式实现,对于对象的客户来说,选择什么样的实现方式并不重要,对象只要提供对外的契约就可以了。换句话说,对象的抽象应该优先于它的实现。而实现则应该仅作为这种抽象后面的私有信息并对绝大多数客户隐藏。
2.封装
封装(Encapsulation)是指:在构造对象的结构和行为的过程中,需要明确地定义对象的外部可见部分和不可见的部分,这可以使对象的接口部分和实现部分相分离,从而降低对象与其客户之间的耦合。
例如,在C++程序设计语言中,类中的属性和方法的可见性(公共、私有和保护)实际上就是封装的一种实现机制。
抽象和封装是两个互补的概念,抽象通常描述的是对象的外部可见行为,而封装关注的则是这些外部行为的实现。封装一般通过信息隐藏加以实现,信息隐藏是将那些不涉及对象本质特征的秘密都隐藏起来的过程。
在通常情况下,一个对象的结构是对外不可见(隐藏)的,其方法的实现也是外部不可见(隐藏)的,只有其外部行为是外部可见的。抽象描述的是对象能够做什么,而封装则是为了使程序可以借助最少的工作进行可靠的修改。
例如,在设计数据库应用程序时,通常不需要关心数据在数据库中的物理表示,而是仅对数据的逻辑结构进行编程。这确保了数据库数据的物理独立性。
在实践中,每个类必须有接口和实现两个部分。类的接口描述了它的外部视图,包含了这个类所有实例的共同行为的抽象。类的实现包括抽象的表示以及实现期望的行为机制。通过类的接口,我们能知道客户可以对这个类的所有实例做出哪些假定。实现了封装的细节,客户不能对这些细节做出任何假定。
信息隐藏的另一个相关的问题是,隐藏还具有层次性。即在一个抽象层次被隐藏起来的内容,在另一个抽象层次里可能代表了外部视图,此时,对象的内部表示就可能被暴露出来。
例如,在C++程序设计语言中,类属性和方法的可见性分为公共、私有和保护三种,事实上,C++还通过友元机制为友元类实例提供了一种更宽泛的接口。这使得C++定义了公共、私有、保护和友元等多个层次的封装性。
在大多数情况下,隐藏层次的划分虽然为对象的设计提供了比较充分的支持,但同时也会为设计带来额外的复杂性。所以,封装层次的运用并不能保证设计出高质量的软件系统。
3.模块化
模块化可以被看成是系统的一种属性,这个属性使得系统可以被分解成一组高内聚和低耦合的模块。模块化过程中,抽象、封装和模块化的原则是相辅相成的。一个对象围绕单一的抽象提供了一个明确的边界,封装和模块化都围绕这种抽象提供了屏障。
在结构化方法中,模块化主要是按照高内聚、低耦合的设计原则对程序进行分组。而在面向对象方法中,模块化的任务通常演变成了对类和对象通过打包(Package)的方式来进行分组,此时每一个包都可以被定义成若干个类(或对象)构成的集合,而且包里面还可以包含其他的包,这与结构化设计的模块化有着明显的不同。
从分支策略的角度来看,将一个程序划分成若干个不同的模块可以在一定程度上降低程序的复杂性。但更重要的是,模块划分可以在程序内部定义出一些结构良好的、具有清晰定义的模块边界。这些模块边界(或接口)对于理解程序是非常有价值的。
模块化的设计原则通常包括:为降低软件的开发和维护成本,每个模块必须可以被独立地进行设计和修改;每个模块的结构都应该足够简单,使它更容易理解;可以在不知道其他模块的实现细节和不影响其他模块行为的情况下,修改某个模块的实现;修改设计的容易程度应该能够满足可能的需求变更。
模块化最重要的一点是:发现正确的类和对象,然后将它们放到不同的模块中,这是最基本的设计决策。类和对象的确定是系统逻辑结构设计的一部分,而模块的划分和确定则是系统物理结构设计的组成部分。我们不可能在物理设计开始之前完成所有逻辑设计,反之亦然。
4.层次结构
任何一个面向对象的系统都包含了类结构(继承)和对象结构(组成)这两种基本的层次结构,类继承描述类之间的继承关系,对象组成则描述对象组成意义上的结构关系。
(1)类结构
类结构是指系统的所有类和这些类之间的关系。类之间的关系中最重要的就是继承(Inheritance)关系。
对于两个类A和B,如果类A拥有类B的所有属性和行为,则称类A和类B之间存在继承关系。此时,称类A是类B的派生类,类B是类A的基类。
从语义上说,继承实际上表明了“是一种”的关系。例如,汽车“是一种”交通工具,快速排序“是一种”排序算法。继承因此实现类之间的一种“一般/具体”的层次结构,其中子类将超类的一般结构和行为具体化。
从继承关系出发,当多个类之间具有比较复杂的继承关系时,这些继承关系将使系统构成一种层次结构,在这个层次结构中,一个子类将继承其超类的所有属性和方法,同时子类也可以扩展或重新定义超类中的结构和行为。
使用继承关系建模一个系统时,可以将多个不同的类中相同属性和方法迁移到它们共同的基类(或超类)中,从而减少这些属性和方法的冗余,并且减少这些类之间的耦合,这也是面向对象方法关注继承关系的一个重要原因。
在这个层次关系中,层次高的类代表了较高层次的抽象,层次低的则代表了较低层次的抽象,同时,低层次类中可以添加新的属性和方法,也可以修改甚至隐藏继承自高层次的类的属性和方法。
通过这种方式,继承可以使得模型(甚至是程序)的描述方式更为经济。一个类通常把其内部分成对外部开放和隐藏的两个部分,开放的部分用接口描述,内部属性和状态则是隐藏的。但继承则要求重新定义一个用于继承的接口,从而允许派生类访问、修改或隐藏其某些状态和方法。
继承机制为类引进了子类这样一种新的客户类类型,但也增加了类封装问题的复杂度。程序设计语言中定义的保护可见性就是一种专门为其子类提供的接口。这样既支持了继承,又在最大限度地保护了类的封装性,同时也支持了不同层次的类之间的多态。
(2)多继承(Multi Inheritance)
层次结构中的另一个问题是多继承(Multi Inheritance)问题,即一个类拥有多个基类(或超类)。在很多情况下,多继承是有意义的。
例如,图1-2描述了一个多继承的案例。其中水陆两栖汽车同时继承了汽车和轮船这两个类,水陆两栖汽车还同时还重复地继承了交通工具类的属性和方法。

图1-2 多继承实例
多继承引入了一定的复杂性,即需要关注这些类中的名字冲突和重复继承等问题。
1)名字冲突。当多个超类含有相同名字的属性或操作时,在它们共同的派生类中,就会发生名字冲突的情况。例如,当汽车和轮船这两个类具有相同的属性名或方法名时,在水路两栖汽车类中,就会继承了两个同名的属性和方法。这会给水路两栖汽车类带来额外的设计负担。
2)重复继承。当多个同层次的类具有共同的超类时,它们的共同子类就会发生重复继承的情况。又例如汽车和轮船这两个类就具有共同的超类交通工具,这两个类继承了交通工具类的属性和方法。当水路两栖汽车类同时继承了汽车和轮船这两个类时,它就可能重复地继承了来自交通工具类的属性和方法。
目前,大多数程序设计语言(如Java、C#等)都不支持多继承,对于不支持多继承的语言,可以将多继承结构调整成一个超类加上与其他类的聚合的形式。少数支持多继承的程序设计语言(如C++),则提供了相应的语言机制来解决上述两个问题,具体的设计决策则由程序员来决定是否使用以及如何使用多继承。
(3)对象的层次结构
类继承说明了类之间的一般/特殊关系,而对象层次结构则主要描述对象之间的关系,对象之间的层次关系主要指聚合关系。
聚合(Aggregation)关系描述的是对象之间的层次关系,当一个对象是由另外一些对象组合而成时,则称整体对象是一个聚合对象,整体对象与部分对象之间的关系称之为聚合关系。
当部分对象与整体对象具有相同的生命周期时,又可以称这个聚合关系为组合关系。
将继承和聚合结合在一起可以构成具有强大功能的结构,聚合允许对逻辑结构进行物理分组,而继承允许这些共同的部分在不同的抽象中被复用。
5.类型
类型的概念源于程序设计语言中的数据类型,其主要强调数据的表示方法、取值范围、允许进行的计算以及一个特定的标识符。
将类型与类的概念相比较,二者具有较强的相似性,类型中的数据表示方法与类的属性定义、类型的计算与类的操作、类型标识符与类名等都有很强的相似性。但类型的概念更基本,并且类型的适用范围显然更为宽泛。
面向对象方法仍然把类型视为对象模型中的一个独立要素,即类型被看作是面向对象领域中某些结构成分(如属性、方法的形式参数和返回值等)的一种抽象描述。
面向对象方法中,类型是关于某种结构成分的强制规定,不同类型的结构成分一般不能够互换使用,或者至少它们的互换使用应该受到某种非常严格的限制。
类型可以分为静态类型和动态类型两种。静态类型是指所有变量和表达式的类型在编译时就确定下来的数据类型。而动态类型(迟后绑定)是指变量和表达式的类型直到运行时刻才能够确定下来的数据类型。也可以将静态类型称为强类型,将动态类型称为弱类型。
另外,多态(Polymorphism)也是动态类型和继承互相作用时所表现出来的一种情形。多态代表了类型理论中的一个概念,即一个名字(或变量)可以代表许多个不同类型的对象,这些类具有某个共同的超类。因此这个对象可以响应一组共同的操作。
多态可能是面向对象语言中除了对抽象的支持以外最强大的功能,也正是它,区分了面向对象编程和传统的抽象数据类型编程。在接下来的章节中可以了解到,多态是面向对象设计中的最重要的核心概念之一。
6.并发
在并行系统中,通常要考虑程序并行或并发执行方面的问题。进程是指一个程序的一次可并发的执行活动,进程通常是由操作系统独立地进行管理的,每个进程都有独立的地址空间。进程之间可以进行通信,进程控制机制通常是由操作系统提供的,所以进程的通信开销比较大,涉及进程间通信技术。而线程则是某种轻量级的进程,线程一般共处于同一进程空间之内,它们共享同样的地址空间和资源,这使得线程之间的通信开销较小,但需要解决共享数据资源的并发控制问题。
设计一个含有多线程特性的大型软件会有更多的困难,因为设计者必须考虑并发控制方面的问题,即系统的死锁、饥饿、互斥和竞争条件等问题。
在面向对象系统中,可以将并发和并发控制(进程或线程)封装在可复用的抽象中。
从并发的角度出发,封装了某个并发的进程或线程的对象称为主动对象,否则称为被动对象。
在面向对象的设计中,共有三种实现并发的方式。
(1)并发程序设计语言
在这种情况下,并发是程序设计语言的内在特征,该种语言本身就提供了并发和同步控制机制。可以直接创建一个主动对象,它与其他主动对象一起并发执行某些处理过程。
(2)软件开发环境提供类库
目前,大多数软件开发环境均以提供类库的方式支持并发程序设计。当然,这种类库通常是平台相关的,也可能是不可移植的。在这种方式下,并发并不是语言的内在特征,但提供的这些标准类,使得并发像是对象的内在特征。其并发的本质在于软件的运行环境对并发的支持。
(3)中断机制
最后一种方式是利用中断机制来实现并发。这种方法是一种最古老的并发方式,使用中断机制,不仅要求程序员具有某些底层硬件细节方面的知识,同时还要考虑软件环境是否支持这样的方式。
不论如何实现并发,当在一个系统中引入并发时,必须考虑主动对象之间活动的同步。
例如,如果两个主动对象试图同时给第三个对象发送消息,则必须确保使用了某种互斥手段,这样,被调用对象的状态才不会因为两个主动对象的同时访问而被破坏。这是使用并发时必须注意的要点。
在并发的情况下,仅定义对象的方法是不够的,还必须确保这些方法的语义在多个控制线程的情况下仍然有效。
7.持久性
软件中的一个对象通常会占用一定量的存储空间,并在一定的时间内存在。一个对象从建立到消亡的时间间隔称为对象的生命周期(Life Time)。
按照对象在软件中的生命周期可以把对象分为如下几种类型。
1)瞬时对象,只在某个时刻存在的对象,如表达式计算使用的临时对象。
2)过程对象,仅存在于某个过程的局部对象。这个过程开始时,对象被创建,这个过程结束时,这些对象被自动销毁,如某个过程的局部变量。
3)全局对象,在软件运行期间全局存在的对象,如软件中的全局变量、堆中的值,它们的存在性和可见性可能会有所不同。
4)持久对象,存在性与软件的运行状态无关的对象。这类对象通常需要数据文件或数据库技术的支持,当软件处在运行状态时,这些对象会根据需要被调入内存并参与系统的运行,在程序运行结束后,这些对象的属性数据将被保存在数据文件或数据库中。
我们将实现对象在数据文件或数据库与内存之间转换的技术称为对象的序列化或持久化。
除了上述四种类型的对象之外,某些商业软件还需要处理存在于不同版本的软件之间的数据兼容性问题,如Microsoft Office Word软件的版本兼容问题。此时,对象还存在不同版本之间的转换问题。
传统编程语言通常只关注前三种类型的对象存在性问题,持久对象通常会涉及数据文件处理技术或数据库技术。某些面向对象编程语言提供了对持久化技术的直接支持,如Java提供的Enterprise Java Beans(EJB)和Java Data Object(JDO)。
将对象序列化到数据文件只是序列化技术中比较初级的解决方案,因为这种技术不适合处理大量对象的情况。更常用的持久化技术是对象到数据库中的映射。本书将在第9章详细讨论对象与关系数据库的映射(OR映射)问题。
另外,持久性问题要解决的不仅仅是对象的生命周期问题。在实际应用中,这些持久对象还有可能会跨越不同的空间(应用)而存在。在这种情况下,不同的应用都会以不同的方式来解释和处理持久对象。当这些跨越不同应用空间的持久对象被存储在数据库中时,显然会增加保持数据完整性的难度。
例如,一个Word文档可以看成是由一组Word文档对象的属性数据组成的集合,这些数据也可以被Word软件使用,但某些其他软件(如WPS软件)也可以处理这个文档的数据,并且至少这些软件处理这些数据的方式是一样的,这些数据在不同的应用中的语义是相通的。但不同软件中定义的封装这些文档对象数据的类却不太可能会完全相同,至少这些类的方法不会完全相同。
这个例子说明了持久对象的持久性还包含了空间方面的属性。
对于分布式系统来说,有时候还必须考虑跨越空间的对象转换问题。例如在分布式系统中,对象从一个节点迁移到另一个节点时,它们在不同节点中甚至会有非常不同的状态和行为。
综上所述,我们可以给出如下的持久性的定义。
持久性是对象本身所具有的一种特性,通过这种特性,对象可以跨越时间和空间而存在。
本小节详细介绍了对象模型的各个构成要素,所有这些要素共同构成了完整的面向对象的核心概念。