C#面向对象程序设计(第2版)
上QQ阅读APP看书,第一时间看更新

1.2 面向对象的基本概念

本节介绍面向对象技术中一些最为基本的概念。

1.2.1 对象

客观世界中的事物都是对象(Object),这既包括有形的物理对象(如一个人、一只狗、一本书等),也包括抽象的逻辑对象(如一个几何图形、一项商业计划等)。对象一般都有自己的属性(Attribute),而且能够执行特定的操作(Operation)。例如,一个人可以描述为“姓名张三,身高170,体重65”,这里的“姓名”、“身高”、“体重”就是对象的属性,而“张三”、“170”、“65”则是对应的属性值;该对象还可以执行“走路”、“看书”等操作。属性用于描述对象的静态特征,而操作用于描述对象的动态特征。

在面向对象的模型中,软件对象就是对客观世界中对象的抽象描述,是构成软件系统的基本单位。但软件对象不应也不可能描述现实对象的全部信息,而只应包含那些与问题域有关的属性和操作。例如,在一个学籍管理系统中,通常会关心每个“学生”对象的“姓名”、“学号”、“专业”等属性信息,而他们的“发型”、“鞋号”等信息则不属考虑范围。

1.2.2 类

类(Class)是指具有相同属性和操作的一组对象的集合,它描述的不是单个对象而是“一类”对象的共同特征。例如在学籍管理系统中可以定义“学生”类,而“张三”、“李明”、“王娟”这些学生就是属于该类的对象,或者叫做类的实例(Instance);它们都具有该类的属性和操作,但每个对象的属性值可以各不相同。

类是面向对象技术中最重要的结构,它支持信息隐藏和封装,进而支持对抽象数据类型(Abstract Data Type, ADT)的实现。信息隐藏是指对象的私有信息不能由外界直接访问,而只能通过该对象公开的操作来间接访问,这有助于提高程序的可靠性和安全性。例如“学生”类可以有“生日”和“年龄”这两个属性,那么可以把它们都定义为私有的,不允许直接修改;再定义一个根据生日计算年龄的私有操作,以及一个修改生日的公共操作,这样用户就只能通过对象的生日来间接修改其年龄,从而保证其年龄和生日的合法性。

类将数据和数据上的操作封装为一个有机的整体,类的用户只关心其提供的服务,而不必了解其内部实现细节。例如对于“借书证”类的“刷卡”操作,用户可以只关注该操作返回的借书人信息(如借书权限、是否欠费等),而不去管磁条中是怎样存储借书人的有关信息的。

1.2.3 消息和通信

对象具有自治性和独立性,它们之间通过消息(Message)进行通信,这也是对客观世界的形象模拟。发送消息的对象叫做客户(Client),而接收消息的对象叫做服务器(Server)。按照封装原则,对象总是通过公开其某些操作来向外界提供服务;如果某客户要请求其服务,那么就需要向服务器对象发送消息,而且消息的格式必须符合约定要求。消息中至少应指定要请求的服务(操作)名,必要时还应提供输入参数和输出参数。例如,某“学生”对象需要办理借书证,那么就要请求“图书馆”对象的“办理图书证”服务,并在消息中提供自己的姓名、学号等消息;“图书馆”对象检查这些消息合格后,创建一个新的“借书证”对象并返回给该学生。

1.2.4 关系

在很多情况下,单个对象是没有作用的。例如“轮子”、“车厢”、“发动机”等对象单独放在那里都没有什么意义,而只有将它们组成一个“汽车”对象才能发挥各自的作用。再如“学生”对象也不能是孤立的,而是要和“班级”、“老师”、“考试”等对象进行交互才能有意义。对象之间的关系可在类级别上进行概括描述,典型的有以下几种。

• 聚合(Aggregation):一个对象是另一个对象的组成部分,也叫部分—整体关系,如“轮子”与“汽车”的关系,“学生”和“班级”的关系等。

• 依赖(Dependency):一个对象对另一个对象存在依赖关系,且对后者的改变可能会影响到前者,如“借书证”对象依赖于某个“学生”对象,当“学生”对象不存在了(如该学生毕业或退学),相应的“借书证”对象也应被销毁。

• 泛化(Generalization):一个对象的类型是另一个对象类型的特例,也叫特殊—一般关系,其中特殊类表示对一般类内涵的进一步细化,如“学生”类可进一步细化为特殊的“本科生”和“研究生”类。从泛化关系可以引出面向对象方法中另一个重要概念——继承。

• 一般关联(Association):对象之间在物理或逻辑上更为一般的关联关系,主要是指一个对象使用另一个对象的服务,如“老师”和“学生”之间的教学关系。根据语义还可将关联关系分为多元关联和二元关联,二元关联还可进一步细分为一对一关联、一对多关联以及多对多关联等。聚合和依赖有时也被视为特殊的关联关系。

上述4种关系的简单示例如图1-1所示。

图1-1 聚合、依赖、泛化和关联关系示例

1.2.5 继承

在泛化关系中,特殊类可自动具有一般类的属性和操作,这叫做继承(Inheritance);而特殊类还可以定义自己的属性和操作,从而对一般类的功能进行扩充。例如“学生”类可以从“人”这个类中继承,这样就继承了“人”的“姓名”、“身高”等属性,而“学号”、“专业”等则是“学生”类自己特有的属性。在类的继承结构中,一般类也叫作基类或父类,特殊类也叫作派生类或子类。

继承的概念是从生物学中借鉴而来的,它可以具有多层结构。例如,动物可分为脊椎动物和无脊椎动物,脊椎动物又可分为哺乳动物、鱼类、鸟类、爬行动物、两栖动物等,这种划分可以持续很多层。在分类过程中,低级别的类型通常继承了高级别类型的基本特征。图1-2所示为这一简单的动物继承关系。不过,在实际软件系统的建模过程中,继承的层次结构不宜过细过深,否则会增加理解和修改的难度。

图1-2 动物的继承关系图

继承具有可传递性。例如“学生”类从“人”类继承,“研究生”类再从“学生”类继承,那么“本科生”和“研究生”类也就自动继承了“人”的“姓名”、“身高”等属性。这样派生类就能够享受其各级基类所提供的服务,从而实现高度的可复用性;当基类的某项功能发生变化时,对其的修改会自动反映到各个派生类中,这也提高了软件的可维护性。

自然界中还存在一种多继承的形式,例如,鸭嘴兽既有鸟类的特征又有哺乳动物的特征,那么可以把它看成是鸟类和哺乳动物共同的派生类;再如一名在职研究生可能同时具有老师和学生的身份。在面向对象的软件开发中,多继承具有较大的灵活性,但同时也会带来语义冲突、程序结构混乱等问题。目前,C++和Eiffel等语言支持多继承,而Java和C#等则不支持,但它们可通过接口等技术来间接地实现多继承的功能。

1.2.6 多态性

多态性(Polymorphism)是指同一事物在不同的条件下可以表现出不同的形态。在面向对象的消息通信时,发送消息的对象只需要确定接收消息的对象能够执行指定的操作,而并不一定要知道接收方的具体类型;接收到消息之后,不同类型的对象可以作出不同的解释,执行不同的操作,从而产生不同的结果。例如,学籍管理系统在每学期开始时会要求每个学生对象执行“选课”操作,但系统在发送消息时并不需要区分学生的具体类型是本科生还是研究生,而不同类型的学生会自行确定自己的选课范围,因为“本科生”类和“研究生”类会各自定义不同的“选课”操作。多态性特征能够帮助我们开发灵活且易于修改的程序。

1.2.7 接口和组件

随着软件规模和复杂度的不断增长,现代软件开发越来越强调接口(Interface)和组件(Component)技术,而接口也已成为面向对象不可或缺的重要元素。组件是指可以单独开发、测试和部署的软件模块,接口则是指对组件服务的抽象描述。一个组件中可以只有一个类,也可以包含多个类。

接口是一种抽象数据类型,它所描述的是功能的“契约”,而不考虑与实现有关的任何因素。例如,可以定义一个名为“图书借阅”接口,并规定其中包括“图书目录查询”、“借书”和“还书”这三项功能。一个类如果声明支持某接口,它就必须支持接口契约中规定的全部功能。例如“图书馆”类要声明支持“图书借阅”接口,它就至少要为“图书目录查询”、“借书”和“还书”这三项操作提供具体的实现机制。

接口一旦发布就不应再作修改,否则就会导致所有支持该接口的类型都变得无效。而组件一经发布,也不应取消它已声明支持的接口,而是只能增加新的接口。例如“图书借阅”接口发布后,我们不能简单地试图为该接口增加一项“图书复印”功能,否则很多已支持该接口的类型就会出错;更合理的方式是定义一个新的“图书复印”接口,那些具有复印能力的类型可以声明支持这个新接口,而不具备复印功能的类型则保持不变。

对于服务的使用方(客户)而言,它既不关心服务提供者的实际类型,也不关心实现服务的具体细节,而只需要根据接口去查询和使用服务即可。例如读者可以向任何一个声明支持“图书借阅”接口的对象发送图书查询请求,并在查到自己所需图书后发送借阅请求,而不必考虑服务的提供者究竟是图书馆、书店还是别的什么机构。接口将功能契约从实现中完全抽象出来,能够有效地实现系统职责分离,同时弥补继承和多态性的功能不足,进而实现良好的系统设计。