3第1章面向对象范式1.1概述(本章的内容)本章将通过与另一种常见的范式——标准结构化程序设计——的比较与对比,向你介绍面向对象范式。过去使用标准结构化程序设计时,开发者们遇到了很多难题,这促成了面向对象范式的产生。通过清楚的了解这些难题,我们可以更好的看到面向对象程序设计的优点,同时对这一机制获得更好的理解。本章不能让你成为面向对象方法的专家。它甚至不能为你解释面向对象领域所有的基础概念。但是,它将帮助你为阅读本书的其他部分做好准备。而本书将向你解释经过专家实践的面向对象设计方法的正确用法。在本章中,我将:l讨论一种常用的分析方法——功能分解。l提出一个实际问题的需求。在这个问题中我们需要处理变化的情况(程序设计苦难的根源!)。l描述面向对象范式,并在实际问题中展示它的应用。PDFcreatedwithFinePrintpdfFactorytrialversion指出特殊的对象方法。l在第21页把本章使用的重要对象术语列成一个表。1.2面向对象范式之前:功能分解(功能分解是处理复杂问题的一种自然的方法)让我们从试用一种常用的软件开发途径开始。如果我给你一个任务:编写代码访问存储在数据库中的几何形状的描述,再把得到的几何形状显示出来。很自然你会考虑分步骤解决这个问题。举个例子,你可能会想按照下面的步骤来做:1.在数据库中查找几何形状的列表。2.打开形状列表。3.以某种规则将这个列表排序。4.在显示器上显示单个的几何形状。你还可能把上面的任何一个步骤再次细分,以方便实现。例如,你可能把步骤4分解成下面的子步骤:对列表中的每个形状,按照下面子步骤做:4a识别形状的具体类型。4b获得形状的位置4c调用适当的函数,并传递形状的位置给它,来显示这个形状。上面这种方法叫做“功能分解”,因为分析者将问题拆分(分解)成多个功能步骤。这些步骤组合起来就可以解决问题。你和我都这么做,因为把问题分成小块来解决,比一次处理整个问题要简单。这种方法,与我写一个制作lasagna(译注:一种意大利PDFcreatedwithFinePrintpdfFactorytrialversion烤面条)的配方的方法、装配自行车的指令的方法是一样的。我们如此经常如此自然的使用这种方法,以至于我们很少对它提出疑问,很少问问自己是否有其他的选择。(这种方法的难题:处理变化)使用功能分解的问题是:它不能帮助我们为未来可能发生的变化做准备,它不能帮助我们的代码优雅的演变。变化会发生,通常是因为我想为一个已经存在的场景添加新的变化。比如说,我可能必须处理新的形状或是显示形状的新方法。如果我把实现上面这些步骤的所有逻辑都放在一个大的函数或模块中,那么,这些步骤中的任何一个发生任何变化,我都必须改变这个函数或模块。变化的发生还为错误和意外结果的发生创造了机会。或者,正如我所说的,许多错误都来自于代码的变化。你可以自己验证这句话。想想这样的时候:你想在代码中做一些改变,但又不敢这么做,因为你知道对一个地方代码的修改可能在另一个地方造成破坏。为什么会这样?是否写一处代码时,必须注意它所有的函数和所有使用这些函数的方式?函数怎样与另一个函数交互?对于一个函数,是否有太多的细节——诸如尝试实现的逻辑、交互的对象、使用的数据等等——需要去注意?就跟人一样,试图同时关注过多的东西,就等于邀请错误与变化同时来到。并且不管你多么努力的尝试,不管你把分析做得多么好,你永远也无法知道用户所有的需求。关于未来有太多的未知数。事物总是在变化的。是的,它们总是在变化……并且你没有任何办法可以阻止它们变化。但你未必一定要被这种变化打败。PDFcreatedwithFinePrintpdfFactorytrialversion需求的问题(需求总是发生变化)如果你去请教一群软件开发者:对于从用户那里获取的需求,哪些是他们知道确实可靠的?他们经常会说:l需求总是不完整的。l需求经常是错误的。l需求(以及用户)都会使人迷惑。l需求不会讲出整个故事。有一句话你永远不会听到:“我们的需求不但完整、清晰、容易理解,而且它指出了我们在未来五年内需要的所有功能!”在我编写软件生涯的三十年中,关于需求我学到的最主要的东西就是:需求总是发生变化。我还发现,绝大多数开发者把这种变化看成一件坏事。其中,很少有人能让自己的代码令人满意的处理变化。需求发生变化的理由非常简单:l由于与开发者进行讨论并且看到软件中新的可能性,用户对自己需求的看法发生了变化。l因为要开发软件来提高用户问题领域的自动化程度,开发者对问题领域更加熟悉,所以开发者对问题领域的看法发生了变化。l软件开发的环境发生了变化。(谁能预测五年以后的WEB环境会变成什么样子呢?)PDFcreatedwithFinePrintpdfFactorytrialversion这并不是说,你和我就可以放弃尝试收集好的需求。这只是意味着我们必须让我们写出的代码能够适应变化。这还意味着我们(和我们的客户)不应该再为这些必然会发生的事情而费尽心力了。变化发生了!想办法处理。l在所有的案例(除了最最简单的案例)中,需求总会发生变化,无论你初期的分析做得多么好。l与其抱怨总是变化的需求,我们更应该改进开发过程,这样我们可以更有效的应付需求的变化。1.4处理变化:使用功能分解(用模块化来包容变化)让我们更近些看看“显示形状”的问题。我怎样写代码才能更容易的处理变化的需求呢?与其写一个庞大的函数,我宁愿把程序写得更模块化。例如,对于第4页上的步骤4c“调用适当的函数,并传递形状的位置给它,来显示这个形状”,我可以写一个如例1-1的模块。例1-1用模块化包容变化—————————————————————————————————function:displayshapeinput:typeofshape,descriptionofshapeaction:switch(typeofshape)casesquare:putdisplayfunctionforsquareherecasecircle:putdisplayfunctionforcirclehere————————————————————————————PDFcreatedwithFinePrintpdfFactorytrialversion这样,当我得到一个需求,需要可以显示一种新的形状——例如三角形——时,我(希望)只需要改变这一个模块。(在功能分解的方法中,模块化的问题)但是这种方法仍然存在问题。比如说,我曾经说过,这个模块的输入是形状的类型和描述。但是我也许可以得到对所有形状都适用的一致的描述,也许得不到,这取决于我如何存储形状的相关数据。如果对形状的描述被存储为包含坐标点的数组会怎么样?这种存储方式能对所有形状都适用吗?很明显,模块化可以帮助你写出更容易理解的代码,更容易理解的代码也更容易维护。但是模块化并不能帮助你写出能应付所有可能出现的变化的代码。(低内聚,紧耦合)迄今为止我一直在使用这种方法,但我发现它给我带来了两个大问题,用术语描述出来即低内聚和紧耦合。在CodeComplete一书中,SteveMcConnell对内聚和耦合做了精彩的描述。他说:l内聚度是指“程序中的操作之间联系紧密的程度”。1我还曾经听到另外的人把内聚度称为透明度,因为操作与程序(或类)的联系越紧密,理解其中的含义就越容易。l耦合度是指“两个子程序之间联系的强度。耦合度与内聚度成反比。内聚度描述了一个子程序的内部成分之间相互联系的强度。耦合度描述了一个子程1McConnell,S.,CodeComplete:APracticalHandbookofSoftwareConstruction,Redmond:MicrosoftPress,1993,p.81.(注:McConnell并不是发明这些术语的人。我们只是最喜欢他对这些术语的定义。)PDFcreatedwithFinePrintpdfFactorytrialversion序与其他子程序之间联系的强度。我们的目标是:创建具有内部完整性(强内聚)的子程序,以及小的、直接的、可见的、灵活的与其他子程序之间的联系(松耦合)。”2(改变一个函数乃至函数使用的数据,都可能引起对其他函数的严重破坏)大多数程序员都有过这样的经验:在代码中的一个地方,对一个函数或一个数据结构做了改动,却对其他地方的代码造成了意料之外的影响。这一类型的bug被称为“多余的副作用”。那是因为当我们获得我们需要的效果(变化)时,我们还得到了我们不需要的效果——bugs!更糟糕的是,这样的bug往往很难被发现,因为通常我们没有注意到引发副作用的程序之间的联系(如果我们注意到了这些联系,我们就不会用这种有副作用的方式来修改程序了)。实际上,这一类型的bug带给我一个相当令人吃惊的发现:我们实际上并没有耗费很多时间来改正程序的错误。我认为,在维护和调试过程中,改正错误只需要短短的一段时间。维护和调试的绝大多数时间都被耗费在寻找错误并防止再次出现副作用上了。真正的改正过程是相当短的!因为副作用往往是最难被发现的bug,所以,如果你让一个函数接触很多不同的数据,在需求发生变化的时候,它就很可能出现问题。2同注1,p.87PDFcreatedwithFinePrintpdfFactorytrialversion函数的一个重要问题就是可能导致很难发现的副作用。l维护和调试阶段的大多数时间不是花在修改错误上,而是花在寻找错误和考虑如何避免在修改中再次引发副作用上。(功能分解将注意力集中在错误的地方)使用功能分解时,变化的需求会让我软件开发和维护的成果大受打击。我主要把目光聚集在功能上。一个函数或数据结构的变化会影响其他的函数和数据,于是受影响的部分也需要修改。这就好象一个滚下山的雪球。只关注功能,结果是一处变化引起很难逃避的瀑布般的变化。1.5处理变化的需求(人们怎样行事?)为了找出一个办法来解决变化的需求造成的问题,也为了弄清楚究竟有没有功能分解之外其他的选择,让我们先来看看现实生活中人们是如何做事的。我们假设你是一个学术联合会(conference)的讲师。参加你的课程的人在你的课之后还将参加其他的课程,但他们不知道下一节课上课的地点。你的责任之一,就是确保每个人都知道到哪里去上下一节课。如果你遵循结构化程序设计的方法,你可能会这样做:1.获得课堂上人的名单。2.对于名单上的每个人:a.查找他的下一节课程。b.查找下一节课程的地点。PDFcreatedwithFinePrintpdfFactorytrialversion查找到他下一节课的教室的路径。d.告诉他怎样去上他的下一节课。为了实现上面这个过程,你可能需要:1.获得课堂上人的名单的方法。2.获得每个人的课程表的方法。3.一个程序来告诉某个人如何从你的教室到另外任何一个教室。4.一个控制程序来为每个人做需要的步骤。(对于你会遵循这种方法的怀疑)我怀疑你是否会真的按照上面这样的方法来做。实际上,你可能会把从你的教室到其他教室的路径张贴出来,然后告诉课堂上的所有人:“我把其他的课程和相应教室的地址张贴在教室后面了。请按照这份地址表去你们的下一个教室。”你期望每个人都知道他们的下一节课是什么。这样他们可以在表上查到他们应该去的教室,并根据表上给出的地址自己走到应该去的教室里去。这两种方法有什么不同?l用第一种方法——显式的为每个人提供导向——你必须密切注意许多细节。除了你之外的任何人对任何事没有任何责任。你会疯掉的!l用第二种方法,你给出一个普遍的指令,并期望