《设计模式解析》第1章 面向对象范型
第1章
面向对象范型
概述
本章通过和另外一种你熟悉的范型——标准结构化编程相对比,来向你介绍面向对象范型。
面向对象范型的产生是因为使用标准化结构编程,过去的实践面临着挑战。通过清楚地了解这些挑战,我们便能更好地看到面向对象编程的优势,并更好地理解它的机制。
本章不会令你摇身一变,成为面向对象方法专家。它甚至不能向你介绍全部的基本面向对象观念。然而,它会让你热热身,为本书的其余章节做好准备,以便向你解释专家们是如何在实践中恰当地使用面向对象设计方法的。
在本章,
l 我将讨论一个普遍的分析方法,功能分解。
l 我将处理需求问题并强调处理变化的需要(编程的灾难!)。
l 我将描述面向对象编程范型并展示其实际使用。
l 我将指出特殊的对象方法。
l 我将在第21页提供一个表,列举本章使用的重要对象术语。
在面向对象范型之前:功能分解
让我们从检验软件开发的一个普遍方法开始。假设我给你一项编码任务去访问一个存储于数据库中的形状描述并显示这些形状。通过思考所需要的步骤可以很自然地想象这项任务。比如,你可能想象你将通过以下步骤来解决这个问题:
1.定位数据库中的形状列表。
2.打开形状列表。
3.根据某种规则排列列表。
4.在显示器上显示单个的形状。
你可以选择这些步骤中的任何一个并将其进一步分解。比如,你可以将步骤4进行如下分解:
4a.识别形状的类型。
4b.得到形状的位置。
4c.调用恰当的形状显示函数,并传送形状的位置。
这被称作功能分解,因为它将问题分解成多个功能步骤。你我之所以这样做,是因为处理小的子问题要比处理整个问题简单。我会运用同样的方法去书写卤汁面条的烹饪方法,或者自行车的组装说明。我们如此频繁、如此自然地使用这一方法,以至于很少质疑它或者询问是否存在别的选择。
功能分解的问题在于它不能帮助我们在将来面对可能发生的改变时,对代码进行优美的演化。改变的需要,往往是因为我想在一个已有的主题上增加一个新的变化量。比如,我可能不得不处理新的形状,或者使用新的显示方法。如果我已经将实现那些步骤的全部逻辑放入一个大的函数或者模块,那么最终对这些步骤的任何改变将导致对该函数或者模块的改变。
此外,改变为错误和计划外的结果创造了机会。或者,正如我想要说的,
很多臭虫源于对代码的改变。
自己检验一下这个断言吧。想象某个时刻你想要作出一个改变但又害怕将该改变置入到你的代码之中,因为你知道在一个地方修改代码可能在别的某个地方破坏掉代码。为什么会这样?难道代码必须关心其全部函数并知道如何使用这些函数?这些函数彼此间是如何交互?岂不是函数需要关心过多的细节,比如它想要实现的逻辑,和它相互作用的事物,以及它使用的数据?对于人来说,试图一次考虑过多的事情,将会在任何事情改变时很容易犯错。
不管你如何努力,不管你做出多么好的分析,你永远不可能从用户那里得到所有的需求。将来有着太多的未知因素。事情在改变。现实总是如此…
你无法阻止改变,但你无需被它征服。
需求问题
询问软件开发人员有关从用户那儿得到的需求,他们总是会说:
l 需求是不完整的。
l 需求通常是错误的。
l 需求(以及用户)是误导的。
l 需求不会告诉你故事的全部。
你永远不会听到的是,“我们的需求不仅完整、清楚、容易理解,而且还给出了未来五年我们将会需要的功能!”。
从我三十年编写软件的经验中,我学到的关于需求的主要东西是…
需求总是在变化。
我得知大多数开发人员总是将这视为一件坏事,但是很少有人写出的代码能够很好地处理变化的需求。
需求之所以变化,是因为一组非常简单的原因:
l 随着和开发人员的讨论以及看到软件上新的可能,用户关于他们需要的观点会发生改变。
l 随着自动化软件的开发,并因此变得更加熟悉问题域,开发人员关于问题域的观点会发生改变。
l 软件的开发环境会改变。(五年前,谁会预料到Web开发会像它今天这个样子?)
这不意味着你我应该放弃收集好的需求。这意味着我们必须写出能够包容变化的代码。这也意味着我们必须停止因为自然而然的事情而痛打自己(或者我们的客户,因为同样的原因)。
变化在发生!处理它。 l 除了最简单的情况,不管我们初始分析做的有多好,需求总是会改变! l 我们应该改变开发过程以便能够更加有效地处理变化,而不是一味地抱怨变化的需求。 |
处理变化:使用功能分解
让我们近距离观察形状现实问题。我应该如何书写代码,以便更容易处理变换的需求?我可以让它更加模块化而不是书写一个大的函数。
比如,对于第2页的步骤4c,我“调用恰当的形状显示函数,并传送形状的位置。”我可能写出一个如例1-1所示的模块。
例1-1 使用模块化来包含变化
function: display shape input: type of shape, description of shape action: switch (type of shape) case square: put display function for square here case circle: put display function for circle here |
这样,当我收到一个需求以显示一种新的形状—比如三角形时,我只需要改变这个模块(但愿如此!)。
然而,这个方法存在一些问题。比如,我说过这个模块的输入是形状的类型和描述。为保证所有的形状运作良好,可能或者可能不需要一个一致的形状描述,这依赖于我如何存储形状。如果形状的描述有时通过一组点阵存储,结果会怎样?这还可以工作吗?
模块化肯定有助于代码更容易理解,而可理解性使得代码更容易维护。但是模块化不会总是有助于代码处理所有可能面临的变化。
对于目前我已经使用的方法,我发现自己存在两个很大的问题,它们可以以术语低内聚和高耦合为依据。Steve McConnell在《Code Complete》一书中给出了一段关于内聚和耦合的精彩描述。他说,
l 内聚指一个例程中“操作之间的关联程度”。
也有人将内聚解释为明确,因为例程中操作之间的关联程度越高,事情就越容易理解。
l 耦合指 “两个例程之间联系的强度”。耦合是内聚的补充。内聚描述一个例程中内
部内容的彼此关联程度。耦合描述一个例程和其它例程的关联程度。我们的目标是创建这样的例程,它有着内部的完整(高内聚)以及和其它例程轻微、直接、可视化、和灵活的关联(低耦合)。
大多数程序员有这样的经验,对某个代码区域中一个函数或者一段数据作出的修改,会对其它的代码片产生意想不到的影响。这种臭虫被称为“有害副作用”。这是因为在得到我们想要的影响时,我们也得到其它我们不想要的影响—臭虫!更加糟糕的是,这些臭虫通常很难发现,因为我们不会首先注意到导致副作用的关系(否则我们就不会以那种方式进行更改了)。
事实上,这类臭虫让我得到一个相当惊人的发现:
事实上,我们并不花费很多时间去修复臭虫。
我认为在维护和调试的过程中,我们只花费很少的时间去修复臭虫。大量的时间被花费在寻找臭虫和试图避免有害副作用上面。相对而言,真正的臭虫修复过程非常之短!
既然有害副作用通常是最难发现的臭虫,那么让一个函数去访问多个不同的数据项,将会使得一个需求上的变化更加有可能引发问题。
魔鬼就在副作用之中 l 将注意力集中在函数上面很可能导致难以发现的有害副作用。 l 在维护和调试的过程中,大量的时间并不是花费在修复之上,而是花费在寻找臭虫和考虑如何避免因修复臭虫而可能导致的有害副作用之上。 |
使用功能分解,变化的需求使得我在软件开发和维护上所付出的努力遭受重创。我的注意力基本上都集中在函数上面。对一组函数或者数据所作的变化会影响到其它组函数以及其它组数据,这反过来又使得其它的函数必须做出改变。正如一个向山下滚去的雪球会不断拾取雪片一样,将注意力集中在函数上会导致一连串难以避免的变化。
处理变化的需求
为找到一个解决需求变化问题的方法并探讨是否存在功能分解的替代品,我们来看看人们是怎样做事情的。假设你是一次研讨会上的教员,在你的课程之后,人们还有别的课程需要参加,但是他们不知道那些课程在哪进行。你的一项职责就是要确保每一个人知道如何参加下一个课程。
如果采用结构化编程方法,你可能会这么做:
1.获得课程人员列表。
2.对列表中的每一个人:
a. 寻找他要参加的下一个课程。
b.寻找该课程举行的地点。
c. 寻找从你的教室到下一个课程的路径。
d.告诉他如何去参加下一个课程。
做这些需要以下程序:
1.一个用以得到课程成员列表的方法。
2.一个用以得到课程上每个成员日程表的方法。
3.一个用以指导从你的教室去任何其它教室的程序。
4.一个控制程序,它为课程上的每个成员服务,并为每个人执行所需的步骤。
我不相信你最终会遵循这一方法。相反,你可能张贴出从一个教室去另一个教室的指示,并告诉课堂上的每一个人,“我已经将下面每一个课程的位置张贴在教室后面了,请根据它们去你们的下一个教室。”你将期望每一个人都会知道他们接下来是什么课程,他们能够从列表中找到他们应该去的教室,并且能够自己遵循那些去教室的指示。
这些方法的不同之处在哪里?
l 在第一个方法中——给每一个人明确的指示——你不得不关心大量的细节。除你以外没有任何一个人需要对任何事情负责。你会变得发疯!
l 在第二种情况中,你给出通用的指示并且期望每个人能够自己找到该怎么做事情。
这里面最大的不同在于职责的转移。在第一种情况下,你需要对每一件事情负责;在第二种情况下,学生需要对他们自己的行为负责。这两种情况必须实现相同的事情,但其组织结构却非常的不同。
这会带来什么样的影响?
为了看清这种职责重组的效果,让我们来看看在新的需求被指定时会发生什么样的事情。
假设我现在需要对参加此次研讨会的研究生给出特殊的指示。假设在参加下一节课程之前,他们需要收集课程评估并将评估结果带到研讨会办公室。此时,我将不得不修改控制程序以区分研究生和普通学生,并对研究生给出特殊的指示。很可能我不得不对这个程序作出相当大的修改。
然而,在第二种情况下(人们对他们自己负责),我将只需要为研究生写一个额外的例程去遵循。那个控制程序将仍然只需说,“去下一个教室。”每一个人将简单地遵循适合他或她的指示。
对该控制程序来说,这是个重大的不同。在第一种情况下,每当有一种新类别的学生,他们需要遵循特殊的指示时,控制程序就不得不被修改。而在第二种情况下,新类别的学生将不得不为他们自己负责。
这里有三种不同的事情促使上述事实的发生。
它们是:
l 人们必须为他们自己负责,而不是控制程序为他们负责。(注意,为达成此点,一个人必须知道他或她是哪一类学生。)
l 控制程序能与不同类型的人交谈(研究生和普通学生)就好像他们是完全一样的。
l 控制程序不需要知道学生在从一个班级去另外一个班级时需要执行的任何特殊的步骤。
为全面理解其中的含义,建立一些术语是很重要的事情。在《UML Distilled》中,Martin Fowler描述了软件开发过程中的三种不同的视角。它们在表1-1中被描述。
表1-1 软件开发过程中的视角
视角 |
描述 |
概念 |
该视角“表示所研究的领域中的概念……应该画一个很少关心或不关心实现它的软件的概念模型……” |
规格 |
“现在我们正看着软件,但我们是在看它的接口,而不是看它的实现。” |
实现 |
现在我们来到代码层面。“这可能是最常使用的视角,但在很多时候,规格视角常常是一个更好的选择。” |
让我们回头看看先前那个“去下一个教室”的例子。注意,作为导师,你是在概念层面上和人们交流。换句话说,你是在告诉人们你要什么而不是怎么去做。然而,人们去参加他们下一个课程的方法却非常的独特。他们在遵循特定的指令,同时也因此工作在实现层面上。
在一个层面(概念)上交流而在另一个层面(实现)上执行,这导致命令的要求者(教师)不需要精确地知道究竟发生了什么,而只需在概念上知道发生了什么。这是非常强大的。我们来看看如何使用这些观念并利用它们来写程序。
面向对象范型
面向对象范型的中心在于对象。每件事情的焦点都集中在对象上面。我的代码围绕对象而不是函数而组织。
什么是对象?传统意义上的对象被定义为带有方法的数据(函数在面向对象中的术语)。不幸的是,以这种眼光来看待对象是非常有限的。我将简单地使用一种更好的定义(又是在第8章,“扩展我们的视野”)。当我谈到一个对象的数据时,它们可能很简单,就像数字或者字符串,或者可能是其它的对象。
使用对象的好处是我可以定义为它们自己负责的事物。(请看表1-2。)对象天生就知道它们的类型。其内部的数据允许它们知道自己处于什么状态,而内部的代码则允许它们正确运转(即做期望做的事情)。
表1-2 对象和它们的职责
对象…… |
职责在于…… |
学生 |
知道自己在哪一间教室 知道自己下节课在哪一间教室 从一间教室到下一间教室 |
教师 |
告诉人们去下一间教室 |
教室 |
拥有一个位置 |
方向提供者 |
给定两间教室,发出从一间教室去另一间教室的指令 |
在这个例子中,我通过寻找问题中的实体来识别对象,通过观察这些实体需要做什么来识别每一个对象的职责(或者方法)。这与通过寻找需求中的名词来发现对象和通过寻找需求中的动词来发现方法的技巧是一致的。我发现这一技巧的用处相当有限,贯穿此书我将展示一种更好的方法。现在正是我们开始的时候。
思考对象是什么的最好方式是把它看作是某种带有职责的东西。一个好的设计规则是,对象应该为它们自己负责并且应该清晰地定义那些职责。这就是为什么我要说一个学生对象,他的一项职责就是知道如何从一间教室去下面的一间教室。
我也可以使用Fowler的视角框架来看待对象:
l 在概念层面,一个对象就是一组职责。
l 在规格层面,一个对象就是一组能够被其它对象或它自己调用的方法。
l 在实现层面,一个对象就是代码和数据。
不幸的是,面向对象设计总是只在实现层面(通过代码和数据)而不是概念或者规格层面被传授和谈论。但是用后面两种方式来思考对象同样具有巨大的威力!
既然对象拥有职责并且对它们自己负责,那么就需要有一种方法告诉对象做什么事情。记住对象拥有和自己相关的数据以及实现功能的方法。一个对象的一些方法将被其它的对象标示为可调用。这些方法的集合称作该对象的public接口。
例如,在教室一例中,我可以写一个Student对象,它有一个名为gotoNextClassroom()的方法。我可能不需要传递任何参数,因为每一个学生需要为他自己负责。这就是说,他可能知道:
l 怎么样才能够移动。
l 如何得到任何其它信息才能够执行该项任务。
一开始只有一种学生—从一个班级到另一个班级的普通学生。注意,在我的教室(我的系统)中将会有许多这样的“普通学生”。但是如果我想有更多种类的学生那会怎么样呢?让每一类学生拥有一组告诉自己他能做什么的方法,这似乎是没有效率的,特别是对所有学生的共同任务而言。
一个更有效率的途径是将所有学生和一组方法相关联,每一类学生都可以使用或者裁剪这些方法以满足自己的需要。我将会定义一个“通用的学生”来包含这些公共方法的定义。这样,我就拥有所有种类的学生,而每一种学生则不得不跟踪他或她自己的私有信息。
用面向对象术语,这一通用学生被称为一个类。一个类是一个对象行为的定义。它包含一个完整的描述:
l 对象包含的数据元素
l 对象能够执行的方法
l 这些数据元素和方法可被访问的形式
因为一个对象的数据元素可变,因此同种类型的每一个对象将会拥有不同的数据,但却拥有相同的功能(在方法中被定义)。
要得到一个对象,我告诉程序我需要这种类型(即该对象属于的类)的一个新的对象。这个新的对象被称为该类的一个实例。创建类的实例被称作实例化。
使用面向对象的途径来写“去下一个教室”的示例程序会更加简单。该程序看起来就像:
1.开始控制程序。
2.实例化教室里的学生集合。
3.告诉该集合让学生去他们的下一个教室。
4.该集合告诉每一个学生去他们的下一个教室。
5.每一个学生:
a. 寻找他的下一个课程在哪个教室
b. 决定如何去那里
c. 到那里去
6.结束。
以上运行良好,直到我需要添加另外一种学生类型,比如研究生。
麻烦来了。看来我必须允许任何类型的学生进入集合(无论是普通学生还是研究生)。我面临的问题是如何让集合访问它的元素。既然我在谈论代码实现,那么这个集合将实际上是一个数组或者拥有某类对象的事物。如果将该集合命名为RegularStudents,我将不能将GraduateStudents置入其中。如果说该集合仅仅是一组对象,那么如何才能保证我不会将错误类型的对象(即不能执行“去下一个教室”的某个事物)包含进去呢?
办法很简单。我需要一个通用的类型,它包含不止一个特定的类型。我需要一个Student类型,它包含RegularStudents和GraduateStudents。使用面向对象的术语,我们称Student为一个抽象类。
抽象类定义了其它相关类能够做的事情。这些“其它”类是代表了一个特定种类的相关行为的类。这样一个类经常被称作具体类,因为它代表概念的一个特定,或者说不变的实现。
在这个例子中,抽象类是Student。具体类RegularStudents和GraduateStudents代表了两种类型的Student。前者是一类学生,后者也是一类学生。
这种关系被称作is-a,它的正式称呼为继承。即RegularStudents类继承自Student。其它的说法还有,GraduateStudents继承自、特化了Student,或者说是Student的一个子类。
换一种方式来说,Student泛化了RegularStudents和GraduateStudents,或者说是它们的基类或者超类。
抽象类是其它类的占位符号。我使用它们定义派生类必须实现的方法。抽象类也能够包含被所有派生类使用的共同方法。不管一个派生类是使用默认的行为还是用自己的行为来代替它,变化都是到此为止(这与对象必须为它们自己负责的说法一致)。
这意味着我能够让控制器包含Students。我们使用Student为参考类型。编译器能够检测到被这个Student引用参考到的任何事物是一种Student。这给出了两个世界中的最大好处:
l 该集合只需要处理Student(因此允许教师对象只和student打交道)。
l 是的,我还是能得到类型检测的好处(只包含那些能够“去下一个教室”的Student)。
l 任何一种Student只需以它自己的方式来实现其功能。
抽象类不仅仅是不被初始化的类 抽象类常常被描述为不经初始化的类。这样的定义是精确的——在实现层面。但那是十分有限的。在概念层面定义抽象类会更加有用,在那里它们是其它类的简单占位符号。 那就是说,它们给我们一个方法为一组相关的类赋予一个名字。这让我们将这组相关的类视为一个概念。 在面向对象范型里,你必须从三个视角层面不断地思考问题。 |
因为对象必须为它们自己负责,因此有很多东西不必暴露给其它对象。此前我曾提到public接口的概念——那些能被其它对象访问的方法。在面向对象的系统里,主要的访问类型有:
l public——任何事物都能够看到它。
l protected——只有这个类及其派生类的对象能够看到它。
l private——只有这个类的对象能够看到它。
这便引出封装的概念。封装经常被简单描述为数据隐藏。对象通常不会将它们的内部数据成员暴露到外部世界(即它们的可视性为protected或者private)。
但是封装不仅仅是数据隐藏。一般来说,封装意味着任何形式的隐藏。
在这个例子中,教师不知道哪些是普通学生,哪些是研究生。学生的类型对教师而言是隐藏的(我正在封装学生的类型)。如你将将会在本书后面所见到的,这是非常重要的概念。
另一个要学到的术语是多态。
在面向对象语言里面,我们经常提到抽象类类型的对象。然而,事实上我们指的是从这些抽象类派生而来的类的特定实例。
因此,当我通过抽象引用概念性地告诉对象去做某件事情时,依赖于该派生对象的特定类型我将得到不同的行为。Polymorphism一词从poly(很多)和morph(形态)派生而来,因此它意味着多种形态。这是一个合适的名字,因为对相同的调用我会得到很多不同形态的行为。
本例中,教师告诉学生“去下一个教室”。而依赖于学生的类型,它们会展现出不同的行为(多态)。
面向对象术语回顾 |
|
术语 |
描述 |
对象 |
一个拥有职责的实体。通过写一个定义了数据成员(和对象关联的变量)和方法(和对象关联的函数)的类(用代码)来实现职责。 |
类 |
方法的仓库。定义对象的数据成员。代码围绕类而组织。 |
封装 |
典型地被定义为数据隐藏。但把它视为任何形式的隐藏更好。 |
继承 |
让一个类成为另一个类的特殊的种类。这些特化类被称作基类(初始类)的派生。基类有时被称作超类,而派生类有时被称作子类。 |
实例 |
类的一个特殊的例子(它总是一个对象)。 |
实例化 |
创建类的实例的过程。 |
多态 |
能够通过同样的方法引用一个类的不同派生类而又得到和被该派生类适合的行为。 |
视角 |
有三种看待对象的不同视角:概念、规格和实现。这些差别有助于理解抽象类及其派生类之间的关系。抽象类定义了如何在概念上解决问题。它同时定义了同它的任何派生对象通信的规格。每一个派生类提供了所需的特定实现。 |
动手进行面向对象编程
我们来重新检验一下本章开头的形状例子。我将怎样用面向对象的方式来实现它呢?记住,我们不得不这样做:
1.定位数据库中的形状列表。
2.打开形状列表。
3.根据某些规则排列该列表。
4.将形状逐个显示在监视器上。
为了以面向对象的方式来解决这个问题,我需要定义以下对象以及它们所拥有的职责。
我需要的对象是:
类 |
职责(方法) |
ShapeDataBase |
getCollection—得到一个指定的形状集合 |
Shape(抽象类) |
display—定义Shape接口 getX—返回Shape的X位置(用于排序) getY—返回Shape的Y位置(用于排序) |
Square(派生自Shape) |
display—显示一个正方形(该对象所代表的) |
Circle(派生自Shape) |
display—显示一个圆(该对象所代表的) |
Collection |
display—告诉所包含的形状去显示 sort—排序该形状集合 |
Display |
drawLine—在屏幕上画一条直线 drawCircle—在屏幕上画一个圆 |
主程序看起来如下:
1.主程序创建数据库对象的一个实例。
2.主程序请求数据库对象寻找我感兴趣的形状集并初始化包含所有形状的一个集合对象(事实上,它将实例化该集合所包含的圆和正方形)。
3.主程序请求集合排列形状。
4.主程序请求集合显示形状。
5.集合请求它包含的每一个形状显示自己。
6.根据我拥有的形状类型,每一个形状显示自己(使用Display对象)。
让我们看看这是如何帮助我们处理新的需求的(记住,需求总是在改变)。考虑以下新的需求:
增加新种类的形状(比如三角形)。为引入一种新的形状,只需要以下两个步骤:
——创建Shape的一个新的派生类以定义该形状。
——在新的派生类内,为display方法实现一个适合该形状的版本。
改变排序算法。为改变形状的排序方法,只需一个步骤:
——修改Collection的方法。每一个形状将会使用新的算法。
底线:面向对象方法限制了需求变更所带来的影响。
封装带来几个优势。它将事物与用户隔离的事实直接意味着:
l 使用事物更加简单,因为用户不需要担心其实现细节。
l 允许实现改变而不需要为调用者担心。(因为调用者一开始就不知道它是如何实现的,不存在任何依赖。)
l 一个对象的内部对于外部对象是未知的——它们被该对象使用以帮助实现对象接口所指定的功能。
最后,我们来考虑当功能改变时出现的不良副作用问题。使用封装,我们能有效的处理该类臭虫。如果我使用封装并且遵循对象为它们自己负责的策略,那么唯一影响对象的方法是调用该对象的一个方法。该对象的数据以及实现其职责的方法和其它对象带来的变化将被屏蔽开来。
封装解救了我们 |
l 我让对象为它们自己的行为负责的越多,控制程序需要负责的事情就越少。 l 封装使得一个对象内部行为的变化对其它对象透明。 l 封装帮助防止不良副作用。 |
特定的对象方法
我已经谈到过被其它对象或对象自己调用的方法。但当对象被创建时会发生什么事情呢?当它们销毁时又会发生什么事情呢?如果对象是自包含的单元,那么让方法去处理这些情况将是一个好主意。
事实上,这些特殊的方法是存在的,它们被称作构造函数和析构函数。
构造函数是一个特殊的方法,它在对象创建时被自动调用。其目的是处理对象的启动。这是对象被强制为它自己负责的一部分。构造函数天生就是一个进行初始化、设置默认参数、建立和其它对象的关系、或者做任何有助于生成一个良好定义的对象事情的好地方。所有的面向对象语言都会在对象被创建时为它寻找一个构造函数并执行。
通过正确地使用构造函数我们可以很容易地排除(或至少减少)未初始化变量。这类错误通常由于开发人员的不小心而产生。通过在整个代码中使用一组一致的地方(对象的构造函数)进行初始化,这就更容易保证初始化的发生。未初始化变量导致的错误很容易修复但却很难发现,因此这样的约定(自动调用构造函数)能够提高程序员的效率。
析构函数是一个特殊的方法,它帮助一个对象在它不复存在,即销毁的时候进行清理性的工作。所有的面向对象语言都会在一个对象被删除时为它寻找一个析构函数并执行。和构造函数一样,析构函数的使用也是对象被强制为它自己负责的一部分。
析构函数典型用于在对象不再被需要时释放资源。由于Java拥有垃圾收集机制(自动清理不再被使用的对象),因此析构函数在Java中就不像在C++中那样重要。在C++中析构函数也会销毁只被对象自己使用的其它对象,这是很普遍的。
总结
在本章,我展示了面向对象如何帮助我们减小转换系统需求带来的影响,并把它和功能分解进行了对比。
我涉及到了面向对象编程中的一些重要概念,并介绍、描述了其中的基本术语。这些对于理解本书剩余部分中的概念至关重要。(参见表1-3和1-4。)
表1-3 面向对象概念
概念 |
回顾 |
功能分解 |
结构化程序员经常使用功能分解进行程序设计。功能分解将问题拆分为一个个更小的功能。每一项功能再被细分直至可管理。 |
需求变更 |
需求变更是开发过程的固有特性。与其指责用户或自己无法胜任收集好而完整的需求这一看上去就不可能的任务,不如使用能够更加有效处理需求变更的开发方法。 |
对象 |
对象通过其职责而被定义。通过为自己负责,对象简化了使用它们的程序的任务。 |
构造函数 和析构函数 |
对象拥有的特殊方法,它们在对象被创建和删除时被调用。这些特殊方法是: l 构造函数,它初始化或设置好一个对象。 l 析构函数,它在对象被删除时清理对象。 所有面向对象语言都使用构造函数和析构函数来帮助管理对象。 |
表1-4 面向对象术语
术语 |
定义 |
抽象类 |
定义一组概念相似的类的方法和共同属性。抽象类永远不会被实例化。 |
属性 |
和一个对象相关联的数据(也被称作数据成员)。 |
类 |
对象的蓝图——定义该种对象的方法和数据。 |
构造函数 |
特殊的方法,在对象创建时被调用。 |
封装 |
任何形式的隐藏。对象封装它们的数据。抽象类封装由它们派生下去的具体类。 |
析构函数 |
特殊的方法,在对象删除时被调用。 |
功能分解 |
一种分析方法,它将问题拆分为一项项更小的功能。 |
继承 |
类被特化的方法,用于使派生类和它们的抽象类相关联。 |
实例 |
类的一个特定的对象。 |
成员 |
类的数据或者方法。 |
对象 |
一个带有职责的实体。一个特殊的自包含的数据、操作数据的方法的持有者。对象的数据被保护起来不受外部对象的破坏。 |
多态 |
相关对象实现特殊于它们类型的方法的能力。 |
超类 |
一个类,其它类由它派生。包含所有派生类将会使用(和可能会覆盖)到的主要属性和方法的定义。 |
原文出处:
宠辱不惊,闲看庭前花开花落;去留无意,漫随天外云卷云舒。