在60年代的软件开发行业,随着所开发的软件复杂度不断提升,使用原先的方法(1)开发出来的软件终于不能满足需要,其所出现的问题是层出不穷,而且由于缺少必要的文档,人们又没办法寻找定位出其中的问题所在。更有甚者,就算是找出其中的问题,但由于软件设计的杂乱不堪,其修改起来也是叫人头大...于是,终于爆发了所谓的软件危机。
危机爆发后,人们认识到之所以出现这类危机的原因,那是因为没有使用一种系统性的方法来规范软件的开发过程,导致开发出的软件缺少架构不明晰,代码编写不规范,文档缺乏等等问题。最终使得开发出来的软件可靠性大大下降,以至于到了不可使用的地步。
为了解决这种问题,北约组织各国的计算机专家于60年代末召开了两次国际会议(NATO Software Engineering Conference(2))来讨论软件危机问题。会议上提出了“software engineering”一词。从此,为解决软件危机问题,诞生了一门新兴的学科——软件工程学。
经历了几十年的发展,软件工程学这门学科,提出了很多实际可用的软件开发方法。这其中,最著名的,也是业界使用最广泛的,当属结构化的方法和面向对象的方法。本文标题内面向过程的分析(POA),其实就是结构化的分析(SA)。
记得我们在前面介绍课程的时候,说过任何程序都是由数据和处理这些数据的方法构成的,那么对于软件开发方法论来说,您仍然可以从这两方面去考虑。简单来说,您要仔细体会在某一个软件开发方法下,人们是如何考虑下面三点的:
本文只重点讲述结构化的方法和面向对象的方法。您如果也有兴趣了解其他方法,还请广泛使用google/baidu。本文在讲述此两方法的时候,将尽力避免繁杂晦涩难懂的理论措辞,而通过一些具体的例子让您明白其中原理。
按照软件生命周期的定义,结构化的方法可以分成三个阶段,即结构化的分析(SA)、结构化的设计(SD)、以及结构化的编程(SP)。由于下面的内容重在讨论结构化的分析,所以标题也只说分析,不说设计和编程。这点在讨论面向对象的方法时,也是一样。
概念上来说,结构话的方法,就是将整个系统分成为若干个功能上独立的模块。然后针对每个模块,自顶向下的,逐步求精其中每一个层次的各个功能步骤。考虑的时候,需要注意下面三个要素(合称IPO):
数据流图(DFD)是进行结构化分析的主要工具。在DFD里面,主要有四个要素:
知道DFD的构成元素后,我们可以通过一个具体的例子来学习如何使用结构化的分析方法。我们的例子名为CBM项目,在此感谢加拿大多伦多大学计算机系的约翰.迈罗包罗斯教授给我们提供了这样一个例子,您可以访问他在多伦多大学的主页。
注意不管您使用的是结构化的方法,还是后面要介绍的面向对象的方法,其分析材料均来自于一个系统的最原始需求描述文档。许修文档的描述详尽程度直接影响着后面的分析质量,进而影响接下来的设计及编码,所以在您开始分析一个项目之前,请务必把需求材料收集全面,需求文档直接用中文或英文等自然语言描述。
现在有一家公司,名为“The Computer Books By Mail Corp.”,其主要业务是作为计算机专业书籍的零售商,接受顾客的订单(顾客通过email或者电话下单)。订单审核通过后,该公司会从仓库里面提取所需要的书籍,发运给对应的顾客。如果仓库里面不具备足够数量的,或者没有所需要的书籍,该公司会向相关的出版商发出订单,以较低的价格购买后再发运给顾客。顾客在收到书籍后,再依据该公司给开具的发票,付清书款。注意,在做生意过程中,公司极重视诚信,在每次业务之前都会进行信用检查。
图一: CBM项目顶层数据流图
图三: 第二次尝试后的第一层数据流图
图三: 第二次尝试后的第一层数据流图
经过了上面例子的分析,您对如何用结构化的方法来分析系统应该有所感觉了。说到底,还是那八个字,自顶向下,逐步求精。但若要在实际项目中熟练运用,还需仔细体会其中思想,时刻注意培训锻炼自己的思维。
回到我们当初考虑问题的着眼点上来,还记得么?数据和方法。那么,您认为DFD图建模的重点是偏向数据呢,还是方法?显然,是方法。那么结构化的思想中,如何去分析系统中数据的呢?需知,DFD中那些信息流都可能包含有更详细的数据项噢,而且各种信息流中都会牵扯上关系。呵呵,这里的答案就是E-R图,中文称之为实体-关系图。相关资料,还请您先查找资料,自行学习吧。E-R图在数据库应用中使用尤其广泛。
对于结构化分析来讲,DFD和ERD是两种主要分析的工具,但是光用这两种工具还不够。为什么?看看我们画的DFD图,我们能用图描述完系统中的所有需求信息么?显然是不行的。我们还得借助另外两项文字性工具:Data Dictionary(数据词典)和Process Specification(处理规约)。前者,用于描述系统中涉及到所有名词定义,每个名词定义都有可能包含有不同的数据项;后者用来描述各个Process的操作步骤,也即操作序列。关于这些,也请您查找资料进行学习。其实,系统分析中,任何一种分析方法,也都是图示搭配文字描述的,这点在后面的面向对象分析中也可得到验证。
结构化的分析(SA)完成了,后面的SD和SP就比较简单了。如果您是用C语言开发,那说白了,其实就是定义C结构和写C函数了。这个并不难,不在本文的讨论范围之内。
关于结构化分析的参考资料,网上还非常多的。我仅给大家推荐一份PDF文档:即 Ed Yourdon 所著的《Just Enough Structured Analysis》,您可于此处下载,仔细研读。
在进入开发行业后,我们的同学不应该让自己只拘泥于某种技术细节,更应关注其背后的思考方法,想方设法地努力掌握底下的思维习惯,这对以后的发展大有裨益。
其实,在软件领域,没有什么比系统分析更能体现总体智慧了。想想看,其他人在那边哼哧哼哧写代码的时候,你却在那千里之外运筹帷幄,这是何等的不一样。我极同意 Ed Yourdon 关于系统分析的论述:
In fact, systems analysis is more interesting than anything I know, with the possible exception of sex and some rare vintags of Australian wine.
所以,将此作为你毕生的兴趣吧。
相较于结构化的方法而言,面向对象的方法是现今开发软件的趋势性方法。它有着和结构化方法截然不同的思维方式,虽说不同,但此种方法却极好理解,因为你我每天都在生活中用着它,只不过不自知罢了。
面向对象,重点自然是对象。那何所谓对象呢?其实,在我们日常生活中所见的任何事物,你都可以作为对象。您的宠物狗,早上刮胡子用的电动剃须刀,上班用的自行车,写程序用的电脑等等。。。放眼望去,进入您眼帘的皆可为对象。
当我们说到这些对象的时候,脑袋里总会浮现它的特定轮廓。说到猫,总知道它有四条腿,两个眼睛,身上有毛,叫声是喵喵的;说到电脑,总会知道有个屏幕,有个键盘,如果是笔记本电脑,还会有一个电池,电池上的电量可能不能坚持多久等等。。。所有这些,都是用来描述你头脑里的那个对象的。确切的说,这些都代表了对象某一方面的性质。所有性质综合起来,也就描述了这个对象。
另外一方面,这些对象都能有自己的某种行为,或者为外界所利用的某种能力。比方你踢了猫一下,猫就会喵喵叫着跑离开你;你打开电动剃须刀的开关,它如果有电的话,就会开始运转;你跨上自行车,用脚踏自行车的脚蹋,自行车它自己就会向前移动;等等诸如此类,都是对象本身所能执行的某种行为。即便看起来好象没有自己行为的石头,也是如此。试想假如石头本身有个不为零的速度,那么按照牛顿运动第一定律,它自己就能继续运动下去。再比如它的温度如果升高到一定程度,其形态就有可能由固态变化到液态等等。
再次回到前面我们所提考虑问题的着眼点上面来。结构化的方法,是把数据和方法分开考虑的,但是面向对象的方法又是如何的呢?你知道,对于一个完整的对象来说,性质和行为必不能分开考虑。它们两者缺一不可,无论丢开了哪个,对象即不成其为对象。其实性质和行为分别对应于数据和方法,可见,面向对象的方法把数据和方法联合起来考虑了。
在面向对象的术语里,对象的数据称之为对象的属性(attribute),对象所能具有的行为称之为方法(method)。将属性与方法合并起来考虑,称之为对象的封装(encapsulation)。
另外注意,对象在执行某种行为的过程当中,通常需要改变这个对象的某些数据,也就是改变整个对象的状态。比方你在使用电动剃须刀的过程中,剃须刀的会电量随之下降。
既然这世界是由对象所组成的,那么对象之间也就必定会有交互。比方你踢猫,你是一个对象;你踢的那只猫也是一个对象。你们两个对象之间就是一种交互。
那这种交互又是如何发生的呢?在你踢猫这个例子中,你踢猫是你这个对象使用自身的踢这个行为,这个行为作用到了猫这个对象身上。猫在被踢后,喵喵叫着跑离开你。在这里,猫这个对象的叫和跑这两个行为得到了执行。那试想,是谁执行了这两个行为?显然是猫,但这里与其说是猫,还不如说是你在执行踢行为的过程中执行了猫的叫行为和跑行为。不是么?难道你踢猫不正是想让猫走开,或者听几声猫的惨叫来取乐?
所以假如你的名字叫Jason,你的猫叫Jack.那么我们可以认为:Jason在踢方法内,调用了Jack的叫方法和跑方法。用面向对象的记号记作:“Jason.踢(Jack)”调用了“Jack.叫()”和“Jack.跑()”。括号中的Jack是Jason对象踢方法的参数,表示踢行为的作用对象。
用另外一种说法,我们认为:Jason在执行踢方法的过程中,给Jack发送了两个消息以作为命令,Jack收到此两消息后,执行了自己的方法。这正是对象之间交互的实质所在,也即对象之间通过发送消息来进行交互。
前面我们讲了将属性和方法结合起来考虑,称之为对象的封装(encapsulation)。但封装的目的更是为了达到信息掩藏(information hiding)。
举个例子,比方你开一辆汽车。你只要踏下油门踏板就好。至于在汽车油门踏板被压下后,内部如何燃烧燃料,汽车如何驱动内燃机做活塞运动,以产生牵引力等等,你是不需要关心的。实际上,它将很多内部细节掩藏起来,而只给你提供某种可以简单易用的接口(interface),用这些接口,你就可以使用汽车所具有的能力--开动跑起来以运东西。
实际上,接口(interface)规定的,是你如何使用这个对象的这种能力。至于对象在其内部如何动作以体现这种能力,那是属于实现(implementation)的范畴。
信息掩藏(information hiding)是面向对象里的关键概念,也是面向对象方法之所以能战胜结构化方法的重要原因。其存在不仅大大简化了外界使用对象的方式,更在于它不允许外界随随便便就去修改对象内部的数据和状态。回想一下前面结构化分析的方法,所有信息流指代的数据都是全局的,也就是说所有的处理都可以去修改这些数据,不管这种修改是出于有意的,还是无意的,总之它没有提供一种禁止修改的机制,这在实际上带来了很多问题。但是面向对象的方法就不同,所有数据都作为属性放在对象内部的,对它们的修改也都是可控的,因为对象本身是可自知的。
人类区别于其他动物的一个关键因素是具备抽象思维的能力。那么既然普世都是由对象所组成的,那么何不将它们按照彼此相近程度分门别类来理解这个世界呢?其实面向对象的思考方法正是这样的。
你的猫叫Jack,"猫和老鼠"里有只猫叫Tom,另外还有只肥猫叫"Garfield"...这所有的猫都是一个个具体的对象,虽然毛色可能不尽相同,但本质上都是会喵喵叫的猫。所以,我们可以将其归为一类:猫。相仿的,你能找出其他很多对象,并归为不同的类(class)。
类(class),顾名思义,乃描述了一系列具有共通性质的东西。比如猫类,其描述的必定是一些长有四条腿的,会喵喵叫的,身上长有一层毛皮的东西。所以类描述了所有对象共通的属性和行为。至于其他的,比方说毛皮的颜色,Jack,Tom,Garfield可能各有不同,也很正常,因为这正是此猫区别于其他猫的所在之处。
倘若我们现在在猫类里面新添加一个名为毛色的属性。那么Tom的灰白,Garfield的橙色,皆为毛色属性的一个实例,对否?所以,何不把这种思维扩散开来,认为Jack,Tom,Garfield等个体皆为猫类的不同实例?:)实际上,正是如此,面向对象的方法里认为对象皆为某个类的不同实例(instance)。
实际上,类(class)有如建筑师手里的设计蓝图,只要有足够的财力和意愿,我们就可以从同一份设计蓝图出发,建造出一样的多幢建筑来。但是,正如哲学家莱布尼茨所说:“世界上没有两片完全相同的叶子”,我们认为它们是几个独立的、不相同的对象。用面向对象的术语来说,从类出发,创建出不同的对象,称之为实例化(instantiation)。
上面我们抽象出了猫类,另外,你也可能抽象出了狗类,牛类等等。这些类里面,有一个共通的性质,那就是胎生。既然是共性,那将它们放在不同的类里,就比较冗余。于是我们再另外抽象出一个类,哺乳动物类,并将胎生等性质放到这里类里面。然后将胎生等性质从猫类、狗类、牛类等类里面删除,接着让它们继承自哺乳动物类。
经过这样的一次继承,猫类等仍然拥有胎生的性质,只不过此性质并非原先存于猫类中的那个胎生性质(已被删除),而是自哺乳动物类继承得来的。不仅类的属性可以继承,方法也可以通过继承得到。
不管是猫,还是狗和牛,都要吃东西。虽然吃的东西可能不尽相同,但是其吃东西的方法,则均是将食物放在口腔里,咀嚼过之后咽下,然后到胃里面消化。。。所以,我们与其将吃方法放在不同的猫类,狗类及牛类里面,倒不如将其放在哺乳动物类里。如此,记号“Tom.吃()”调用到的就是继承来的那个方法。
像前面那个样子,抽取猫类,狗类及牛类的共性,继而抽象出哺乳动物类的方法,其实是一种泛化(generalization),很显然,哺乳动物类的范畴大多了。反过来,我们认为猫类,狗类及牛类则是哺乳动物类的不同特化。
用面向对象的术语来讲,哺乳动物类称为其他三个类的基类,而其他三个类则继承自哺乳动物类,被称为派生类。
继承或者泛化是面向对象世界里类之间的最基本关系,其他关系还有诸如关联,聚合,包含等等。您若有兴趣,可在巨立安技术的课堂里面通过例子学习。
多态(polymorphism)是面向对象方法里最叫人着迷的地方,也是新手最难以理解的地方。那什么是多态呢,多态多态,一个东西多种状态?:)这是纯粹从字面上得来的理解。我们还是举例来说明吧。
前面,我们说对象的时候,说过对象暴露给外界知道的,只是其接口,而非实现。接口只是规定了外界如何去使用对象的能力,而实现则归纳了该对象如何去实现这种能力。比方说,Tom猫有一个叫()接口让外界命令Tom猫去叫。而Tom猫收到这个命令,执行叫方法后,外界得到的是某一种类的空气震动。那么Tom猫是如何实现这个叫()接口的呢,则是震动其声带发出一种特定的震动波,这种震动波经过叫()接口返回给外界后,传到外界其他对象的耳朵里便是喵喵的声音。这是Tom猫对叫()的实现方式,如果Tom猫它自己愿意,它也可以发出另外一种震动波,让外界听到呼呼的声音。
Tom猫是喵喵叫的,Jack猫和Garfield猫也都是喵喵叫的,所以你在抽象出猫类的时候,势必会将叫方法的接口以及实现都放在猫类里边。如此,任何一个由猫类实例化出来的对象A,记号“A.叫()”必然也就是喵喵的。
同样的,你也会在狗类,及牛类里面加上叫方法的接口和实现。和猫类的叫方法相比,其接口叫()都一样,不同的只是各种动物实现叫()接口的方式而已,即它们发出了不同的震动波。好,那既然有这样一个原因,我们何不把叫方法的接口移到基类哺乳动物类那里,而在派生类里保留各自不同的实现?
经过这样的修改后,记号“Tom.叫()”仍然是喵喵的。现在假设有一只名为Bao的狗和一只名为Mo的牛,那么记号“Bao.叫()”和记号“Mo.叫()”仍然会是汪汪的和哞哞的。
好,现在我们用一个代词它来指代某一只哺乳动物,用另外一种记号“它->叫()”表示命令这只哺乳动物执行叫方法。而因为猫、狗、牛等皆属于哺乳动物,所以它可指代猫类、狗类、牛类的任意一个对象。当指代Tom猫时,记号“它->叫()”是喵喵的;当指代Bao狗时,记号“它->叫()”是汪汪的;当指代Mo狗时,记号“它->叫()”是哞哞的;
当它指代不同的派生类对象时,同一个记号“它->叫()”带来的结果确是不一样的。这正是多态的体现呐!所以本段开头用“一个东西多种状态”的论述来理解多态,虽然不甚精确,但也勉强尚可。
以上介绍了面向对象的一些基本观念,不算难,对吧?其实用面向对象的方法就是分离出待解问题中的各个对象,对他们之间的交互建立模型。继而抽象出不同的类,以及不同类之间的不同关系。最后用某种面向对象的语言(诸如C++,Java等等),来实现你所建立的模型。
关于面向对象,现在市面上很多书籍、很多课程一上来就给同学介绍很多高深的工具与语言。殊不知,最重要的是理解这背后的概念。所以花点时间是值得的,掌握好了这些概念之后,您才能慢慢的深入学好其他更多的东西,诸如C++,UML,Design Patterns等等。您若有兴趣,可以联系我们深入学习。
尽管我现在所从事的行业方向是嵌入式linux驱动开发,但我仍努力不断的以OO的方式来分析问题作为第二乐趣。我想,您也许可以和我一样。不过,在涉足OO领域之前,我仍强烈希望您先掌握好基本概念,因为我也极同意 Craig Larman 在文章"What the UML Is--and Isn't"中的论述:
Unfortunately, in the context of software engineering and the UML diagramming language,acquiring the skills to read and write UML notation seems to sometimes be equated with skill in object-oriented analysis and design. Of course, this is not so, and the latter is much more important than the former. Therefore, I recommend seeking education and educational materials in which intellectual skill in object-oriented analysis and design is paramount rather than UML notation or the use of a case tool.
注意:
1在软件开发的最初阶段,甚至都没用上什么方法。那个时候的软件功能都很单一,也都是自己写给自己用的,所以也就不需要什么工程性的方法。
2关于这两次会议的更详细情况,请看这里。