第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 |
这样,当我收到一个需求以显示一种新的形状—比如三角形时,我只需要改变这个模块(但愿如此!)。
然而,这个方法存在一些问题。比如,我说过这个模块的输入是形状的类型和描述。为保证所有的形状运作良好,可能或者可能不需要一个一致的形状描述,这依赖于我如何存储形状。如果形状的描述有时通过一组点阵存储,结果会怎样还可以工作吗/span>
模块化肯定有助于代码更容易理解,而可理解性使得代码更容易维护。但是模块化不会总是有助于代码处理所有可能面临的变化。
对于目前我已经使用的方法,我发现自己存在两个很大的问题,它们可以以术语低内聚和高耦合为依据。Steve McConnell在《Code Complete》一书中给出了一段关于内聚和耦合的精彩描述。他说,
l 内聚指一个例程中“操作之间的关联程度”。
也有人将内聚解释为明确,因为例程中操作之间的关联程度越高,事情就越容易理解。
l 耦合指 “两个例程之间联系的强度”。耦合是内聚的补充。内聚描述一个例程中内
部内容的彼此关联程度。耦合描述一个例程和其它例程的关联程度。我们的目标是创建这样的例程,它有着内部的完整(高内聚)以及和其它例程轻微、直接、可视化、和灵活的关联(低耦合)。
大多数程序员有这样的经验,对某个代码区域中一个函数或者一段数据作出的修改,会对其它的代码片产生意想不到的影响。这种臭虫被称为“有害副作用”。这是因为在得到我们想要的影响时,我们也得到其它我们不想要的影响—臭虫!更加糟糕的是,这些臭虫通常很难发现,因为我们不会首先注意到导致副作用的关系(否则我们就不会以那种方式进行更改了)。
事实上,这类臭虫让我得到一个相当惊人的发现:
事实上,我们并不花费很多时间去修复臭虫。
我认为在维护和调试的过程中,我们只花费很少的时间去修复臭虫。大量的时间被花费在寻找臭虫和试图避免有害副作用上面。相对而言,真正的臭虫修复过程非常之短!
既然有害副作用通常是最难发现的臭虫,那么让一个函数去访问多个不同的数据项,将会使得一个需求上的变化更加有可能引发问题。
魔鬼就在副作用之中 l 将注意力集中在函数上面很可能导致难以发现的有害副作用。 l 在维护和调试的过程中,大量的时间并不是花费在修复之上,而是花费在寻找臭虫和考虑如何避免因修复臭虫而可能导致的有害副作用之上。 |
使用功能分解,变化的需求使得我在软件开发和维护上所付出的努力遭受重创。我的注意力基本上都集中在函数上面。对一组函数或者数据所作的变化会影响到其它组函数以及其它组数据,这反过来又使得其它的函数必须做出改变。正如一个向山下滚去的雪球会不断拾取雪片一样,将注意力集中在函数上会导致一连串难以避免的变化。
处理变化的需求
为找到一个解决需求变化问题的方法并探讨是否存在功能分解的替代品,我们来看看人们是怎样做事情的。假设你是一次研讨会上的教员,在你的课程之后,人们还有别的课程需要参加,但是他们不知道那些课程在哪进行。你的一项职责就是要确保每一个人知道如何参加下一个课程。
如果采用结构化编程方法,你可能会这么做:
1.获得课程人员列表。
2.对列表中的每一个人:
a. 寻找他要参加的下一个课程。
b.寻找该课程举行的地点。
c. 寻找从你的教室到下一个课程的路径。
d.告诉他如何去参加下一个课程。
做这些需要以下程序:
1.一个用以得到课程成员列表的方法。
2.一个用以得到课程上每个成员日程表的方法。
3.一个用以指导从你的教室去任何其它教室的程序。
4.一个控制程序,它为课程上的每个成员服务,并为每个人执行所需的步骤。
我不相信你最终会遵循这一方法。相反,你可能张贴出从一个教室去另一个教室的指示,并告诉课堂上的每一个人,“我已经将下面每一个课程的位置张贴在教室后面了,请根据它们去你们的下一个教室。”你将期望每一个人都会知道他们接下来是什么课程,他们能够从列表中找到他们应该去的教室,并且能够自己遵循那些去教室的指示。
这些方法的不同之处在哪里/span>
l 在第一个方法中——给每一个人明确的指示——你不得不关心大量的细节。除你以外没有任何一个人需要对任何事情负责。你会变得发疯!
l 在第二种情况中,你给出通用的指示并且期望每个人能够自己找到该怎么做事情。
这里面最大的不同在于职责的转移。在第一种情况下,你需要对每一件事情负责;在第二种情况下,学生需要对他们自己的行为负责。这两种情况必须实现相同的事情,但其组织结构却非常的不同。
这会带来什么样的影响/span>
为了看清这种职责重组的效果,让我们来看看在新的需求被指定时会发生什么样的事情。
假设我现在需要对参加此次研讨会的研究生给出特殊的指示。假设在参加下一节课程之前,他们需要收集课程评估并将评估结果带到研讨会办公室。此时,我将不得不修改控制程序以区分研究生和普通学生,并对研究生给出特殊的指示。很可能我不得不对这个程序作出相当大的修改。
然而,在第二种情况下(人们对他们自己负责),我将只需要为研究生写一个额外的例程去遵循。那个控制程序将仍然只需说,“去下一个教室。”每一个人将简单地遵循适合他或她的指示。
对该控制程序来说,这是个重大的不同。在第一种情况下,每当有一种新类别的学生,他们需要遵循特殊的指示时,控制程序就不得不被修改。而在第二种情况下,新类别的学生将不得不为他们自己负责。
这里有三种不同的事情促使上述事实的发生。
它们是:
l 人们必须为他们自己负责,而不是控制程序为他们负责。(注意,为达成此点,一个人必须知道他或她是哪一类学生。)
l 控制程序能与不同类型的人交谈(研究生和普通学生)就好像他们是完全一样的。
l 控制程序不需要知道学生在从一个班级去另外一个班级时需要执行的任何特殊的步骤。
为全面理解其中的含义,建立一些术语是很重要的事情。在《UML Distilled》中,Martin Fowler描述了软件开发过程中的三种不同的视角。它们在表1-1中被描述。
表1-1 软件开发过程中的视角
视角 |
描述 |
概念 |
该视角“表示所研究的领域中的概念……应该画一个很少关心或不关心实现它的软件的概念模型……” |
规格 |
“现在我们正看着软件,但我们是在看它的接口,而不是看它的实现。” |
实现 |
现在我们来到代码层面。“这可能是最常使用的视角,但在很多时候,规格视角常常是一个更好的选择。” |
让我们回头看看先前那个“去下一个教室”的例子。注意,作为导师,你是在概念层面上和人们交流。换句话说,你是在告诉人们你要什么而不是怎么去做。然而,人们去参加他们下一个课程的方法却非常的独特。他们在遵循特定的指令,同时也因此工作在实现层面上。
在一个层面(概念)上交流而在另一个层面(实现)上执行,这导致命令的要求者(教师)不需要精确地知道究竟发生了什么,而只需在概念上知道发生了什么。这是非常强大的。我们来看看如何使用这些观念并利用它们来写程序。
面向对象范型
面向对象范型的中心在于对象。每件事情的焦点都集中在对象上面。我的代码围绕对象而不是函数而组织。
什么是对象统意义上的对象被定义为带有方法的数据(函数在面向对象中的术语)。不幸的是,以这种眼光来看待对象是非常有限的。我将简单地使用一种更好的定义(又是在第8章,“扩展我们的视野”)。当我谈到一个对象的数据时,它们可能很简单,就像数字或者字符串,或者可能是其它的对象。
使用对象的好处是我可以定义为它们自己负责的事物。(请看表1-2。)对象天生就知道它们的类型。其内部的数据允许它们知道自己处于什么状态,而内部的代码则允许它们正确运转(即做期望做的事情)。
表1-2 对象和它们的职责
对象…… |
职责在于…… |
学生 |
知道自己在哪一间教室 知道自己下节课在哪一间教室 从一间教室到下一间教室 |
教师 |
告诉人们去下一间教室 |
教室 |
拥有一个位置 |
方向提供者 |
给定两间教室,发出从一间教室去另一间教室的指令 |
在这个例子中,我通过寻找问题中的实体来识别对象,通过观察这些实体需要做什么来识别每一个对象的职责(或者方法)。这与通过寻找需求中的名词来发现对象和通过寻找需求中的动词来发现方法的技巧是一致的。我发现这一技巧的用处相当有限,贯穿此书我将展示一种更好的方法。现在正是我们开始的时候。
思考对象是什么的最好方式是把它看作是某种带有职责的东西。一个好的设计规则是,对象应该为它们自己负责并且应该清晰地定义那些职责。这就是为什么我要说一个学生对象,他的一项职责就是知道如何从一间教室去下面的一间教室。
我也可以使用Fowler的视角框架来看待对象:
l 在概念层面,一个对象就是一组职责。
l 在规格层面,一个对象就是一组能够被其它对象或它自己调用的方法。
l 在实现层面,一个对象就是代码和数据。
不幸的是,面向对象设计总是只在实现层面(通过代码和数据)而不是概念或者规格层面被传授和谈论。但是用后面两种方式来思考对象同样具有巨大的威力!
既然对象拥有职责并且对它们自己负责,那么就需要有一种方法告诉对象做什么事情。记住对象拥有和自己相关的数据以及实现功能的方法。一个对象的一些方法将被其它的对象标示为可调用。这些方法的集合称作该对象的public接口。
例如,在教室一例中,我可以写一个Student对象,它有一个名为gotoNextClassroom()的方法。我可能不需要传递任何参数,因为每一个学生需要为他自己负责。这就是说
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!