您的位置:首页 > 编程语言 > C语言/C++

Thinking in C++ (1-7) 分析与设计

2007-03-17 19:15 405 查看

第1阶段:分析(我们在做什么)

上一代程序设计(基于过程的设计)中的这一阶段称为进行“需求分析”和书写“系统说明书”。尽管它们的初衷是好的,但是这却成了令人头疼的地方,编写这些连名字都吓人的文档本身就是一项大工程了。需求分析所做的是“列出一个指南,其中指出了什么样的项目能让客户满意。”系统说明书的主要内容是:“满足需求所需要做的事情(而不是如何做这些事情)”需求分析实际上是你和你的客户之间订立的实在的合同。(有的时候,客户有可能是你公司内部的人员,甚至是其他的一些对象,或系统)系统说明书是对问题的总揽,从某种意义上讲是对项目可行性的分析以及对项目开发周期的预计。上述两者需要在你的团队中达成一致,(这是因为随着时间的推移,它们经常会改变)我认为两者越少越好,这样能节约更多时间,最理想的状况是化成列表和基本的图表。可能是出于某种原因你不得不扩大文档的规模,但是最起码应该保证最初的文档小而简洁。可以设立若干小组头脑风暴会议集思广益,这一过程中小组的负责人就可以完成分析文档。这样做不仅征求了大家的意见,而且促使团队达成一致意见。同时可以激发整个团队的开发热情,这或许才是最重要的。
在这一阶段,把注意力放在“我要完成的是什么”上是很重要的:你要确定下来系统的功能究竟是什么。在这一阶段最有价值的工具是“用例(use case)”。用例是用来确定系统的关键特征的,它们可以揭示出一些你将会使用到的一些基础的类。用例是对下面列出的问题的描述性解答[1]
· “谁会使用这个系统?”
· “使用这一系统,参与者能做些什么?”
· “特定的参与者如何做这件事?”
· “如果其它的参与者试图做这件事,或者同一参与者试图作某些其它的事,系统如何运作?”(从而发现系统变化情况)
· “系统运行这些事时会发生什么问题?”(用以发现异常)
比如说你正在设计一个自动提款机,对于系统功能进行详细说明的用例应该可以描述出自动提款机在所有可能的情况下做出的行为。每一种“情况”被称为一个“场景”,一个用例可以看成是一组场景的集合。你可以把场景想象成为这样的一类问题“如果发生了某某事,系统将做些什么?”比如说,“如果一个客户在24小时内存入一张支票,这张支票尚未过户,但此时账目中没有足够的余款可供提取,这个时候自动提款机将做何种处理?”
用例图有意设计得非常简单,这可以防止你过早的陷入考虑系统实现细节的泥沼中。

每个小人表示一个“参与者”,参与者可能是一个人,也可能是其它的什么东西。(甚至有可能是其他的计算机系统,比如在上边的用例图中的“ATM”)方框表示系统边界。椭圆表示用例,就是对系统各个功能的描述。参与者与用例之间的连线表示他们之间的交互的关系。
用户只需要了解系统的功能和使用方法,系统如何实现对他们并不重要。
一个用例并不需要有多复杂,即使底层系统非常复杂也是一样。用例只是用来表示用户看上去的系统是什么样的,比如说:

用例可以通过确定用户与系统之间交互的内容来进行需求说明。你的工作就是尝试找出你的系统所包含的所有的用例,一旦这一工作完成了,你就掌握了这一系统所要实现的功能的核心。关注用例的好处是,它们总能为你指出重点所在,并且可以防止你把精力浪费在对于完成任务无关紧要的事情上。这就是说,如果你拥有了系统的所有的用例,你就可以正确的描述你的系统,并且可以进行分析设计的下一个步骤了。也许你在首次尝试时没有得出全部的用例来,但是没关系。一切将在适宜的时候自我显现,如果此时,你就希望得到一个完美的系统说明书,那你将裹足不前。
此时如果遇到了困境,你可以使用一个近似的手段强制启动这一阶段:通过若干图表描述该系统,同时寻找一些名词和动词,用名词来表示参与者,用例的上下文(比如“休息室”)或者用例的改动。动词则用来表示参与者与用例之间的交互关系,以及指明用例中的步骤。同时你可以发现,这些名词和动词可以帮助创建出设计阶段的对象和消息(要注意,用例描述的是不同的子系统之间的交互关系,所以“名词和动词”这一手段只能作为一个头脑风暴工具,而不能用来创建用例。)[2]
用例和参与者之间的边界存在于用户界面中,但是这一边界并不能定义这一用户界面。关于定义和创建用户界面的更多信息请参见《实用软件》Larry Constantine和Lucy Lockwood著(Addison Wesley Longman, 1999),或登录www.ForUse.com网站。
尽管看上去像是魔术,但是在这一时刻,进行某种基本的进度安排是十分重要的。当你对于你所创建的系统有一个基本的印象时,你也许就可以对系统完成的时间做出一个大概的估计。这里将会涉及到许多因素。比如说:如果你估算出了一个详细的进度表,但是公司却决定放弃这一项目(于是他们就可以把他们的投资用在更有价值的地方,这是件“好”事)。或者一个经理可能也已经决定了这一项目所需要的工期并且试图改变你的想法。对你而言,从最开始就制定一个可信的进度表,并且在早期就做出义无反顾的决定,才是最好的做法。对于创制精确的进度表的方法,人们进行过很多的尝试(好比预测股市行情的方法),但是最好的探索实际上源自你自己的经验和直觉。在项目工期上给出直觉上的判断,然后将这一数字加倍再加10%。如果你的直觉是正确的,那么你将能够在增加后的时间内有所收获。“加倍”将使得产品更加稳定,成熟,其余的10%将用于为产品作最后的润色以及解决一些细节问题[3]。然而,你必须要说服别人,在你揭示这一进度表的过程中,也许周围的人会抱怨甚至强制操控,但这些问题都会自行解决。

第2阶段: 概要设计(如何构建系统?)

在这一阶段,你必须创建一个提纲,它应该能够描述系统中的类以及不同类之间的交互方式。类职责协同(Class-Responsibility-Collaboration, CRC)策略“卡片”。某种程度上讲这一工具的好处就是它非常的简单易用:你可以准备若干张3*5英寸的空白纸片,每一张卡片表示一个独立的类,你可以在上边书写相关内容:
1. 类的名字。这是很重要的,因为通过类的名字可以直接的看出类是做什么的,所以名字是类的功能一目了然。
2. 类的“职责”:类应该做些什么。可以仅仅通过列出成员函数的名称来概述(如果你的设计是优秀的,那么这些名字就应当能够描述出他们自身的功能)。另外,还可以包含其他的信息。如果你期望加快这一进程,那么你需要以一个懒程序员的立场观察这一问题:为了解决你的问题,你需要上帝赠送给你什么样的对象呢?
3. 类的“协同”:其他的类会与本类进行什么样的交互?我有意地使用了“交互”这一较为广义的术语;它可以表示聚合,或者仅仅表示其他的某一对象,这一对象可以对本类的某一对象提供服务。我们也可以将协同视为本类的“听众”。比如,如果你创建了一个类——烟花(Firecracker),谁会关注它呢?是一个化学家(Chemist)还是一个观众(Spectator)?前者主要关心烟花的化学构成,而后者只会对烟花爆炸时的颜色和形状感兴趣。
你可能会认为这些卡片应该包含更多更详细的内容,那样我们就可以从上面找出所有需要的信息,但是我们应该有意地将它们作小,这不仅仅是使你的类尽量的小巧,而且是要防止你过早的陷入在细节问题上的纠缠。如果你不能够在一张小卡片上得到你所需要的所有信息的话,那么就是类设计得太过复杂(不是由于你拘泥于细节,就是这个类有再度分成两个小些的类的可能)。理想的类的特性应该一目了然。CRC卡片的理念就是帮助你迈出工程设计的第一步,然后你就可以对工程的概貌有一定的了解,从而不断改善你的设计。
CRC卡片为你带来了巨大裨益,其中之一体现在交流上。即使没有计算机,小组的成员也可以实时地交流。每个人负责若干个类(每个类可能在一开始甚至没有名字以及其它的任何信息)。通过决定对不同的类发送什么样的消息,从而满足每个场景的需求,一次解决一个场景的问题,你就可以进行对全景的实时模拟。当你在进行这一工作时,你将发现你需要的类以及它们的职责和协同信息,这是你可以在卡片上标出这些信息。当你完成了所有这些用例时,你的设计已经成功地迈出了第一步。
在我开始使用CRC卡片这一工具以前,我在做针对初步设计方面的顾问时,最成功的经验就是,站在一个小组面前,这个小组的成员可能完全没有OOP项目背景,我在黑板上画出一个个对象。我们讨论对象之间应该如何通信,同时,删除一些不合时宜的对象并且适当添加一些新的对象。考虑到效率问题,我将所有的“CRC卡片”粘贴在黑板上。这时小组(了解项目的目标是什么)实际上进行项目的设计;他们“拥有”设计构想,而不是坐享一个现成的构思。我所需要做的只是通过提出正确的问题来指引设计进程的进行,对设想进行斟酌,以及从团队获得反馈以优化现有的设想。这一过程的美妙之处在于,项目团队可以学习到如何进行面向对象设计,不仅仅通过观察一些抽象的例子,更重要的是在此刻他们所做的工作是完成“自己的”设计,这可以充分调动团队的积极性。
当你完成了一组完整的CRC卡片之后,你可能需要对你的设计进行一个更正式的描述,这时你可以使用UML[4]。UML并不是必需的,然而它很有用,尤其是当你希望用投影机打出一个图解让每个人观察并考虑你的设计(这是一个好主意)。除了UML,你还可以使用文字说明书来描述对象及其接口的概要信息,或者就是代码本身[5],这取决于你所使用的编程语言。
UML同时也为描述你的系统的动态模型提供了另外一种符号表示法。这对于分析系统或者子系统(应该是十分重要的子系统,以至于它需要独立的图解,比如一个控制系统)状态转换十分有用。对于那些数据信息占主导地位的系统或子系统(比如数据库),你也许还需要描述数据结构。
你将了解到,在你完成描述对象及其接口的同时,第2阶段的工作就完成了。我指的是这些对象中的大多数——说“大多数”是因为有一小部分对象直到第3阶段才会显现出来。但是没关系,无论如何你最终都会得到你所需要的所有对象。在这一阶段得到的对象是多多益善,但是OOP为我们提供了足够完美的结构,使得即使一些对象发现得晚些对整个项目也不会构成太大负面影响。实际上,软件开发的五个步骤中,都可能发生对象设计这一工作。

对象设计的五个步骤

对象设计的生命周期不仅仅是编程所用的时间。它包含更多的内容,对象设计需要一系列的步骤。用这一观点看问题是很有好处的,因为这样你就不会奢望事情一开始就完美无缺,相反,随着时间的推移,你将逐步了解到对象的特征和行为。这一观点适用于各种类型的程序,对于一个特定类型的程序的设计模式将在反反复复的努力之后显现出来(设计模式(Design Pattern)问题将在第二卷中详细讲解)。同样地,在逐步的理解,使用与复用的过程中,对象的模式也将显现。
1. 对象探索。这一步骤发生于对于一个程序进行内部分析时。通过观察外部因素和边界,系统中的重复元素,以及最小概念单元,你可以发现所需要的对象。如果你已经有一系列的类库,一些对象就是显而易见的。类之间(比如基类与继承类)的共同点将立刻显现出来,或者在设计过程中显现。
2. 对象收集。当你构建一个对象时,你会发现此时需要一些新的成员,而它们在探索过程中从未出现过。对象的内部需求可能需要其它类的支持。
3. 系统构建。再次强调,在后期可能会需要更多的对象。就像你所学到的,你改进你自己的对象。系统里一个对象与另一个对象之间存在着通讯和交互,这可能使你的类的功能做出一些改动,或者创建一些新的类。比如说,你发现你的程序需要一些辅助类,比如说一个链表类,这些辅助类可能不包含任何状态信息,并且仅仅是为了帮助其它类完成他们的功能。
4. 系统扩展。在你试图为系统添加一些新的功能时,你可能会发现你早期的设计并不支持这一扩展。在这种情况下,你可以重新构建系统的某部分,可能的途径包括添加一些新类,或者新的类层次结构。
5. 对象复用。这才是对类的高强度测试。如果一些人试图在一个全新的状况下复用一个类,他们可能会发现这个类存在着一些缺陷。当你为了适应更多的程序而修改一个类时,这个类的基本原则将会变得愈发清晰,直到你拥有了一个真正的可以复用的类型。然而,不要指望大多数对象拥有可复用的特性——繁杂的对象可以仅仅针对一个单一的系统,这是完全可以接受的。可复用的类型看上去似乎更加不常见,同时,为了符合可复用性,它们必须解决掉更加宽泛的问题。

对对象设计的几点建议

当你考虑开发类时,可以参考下边的几条建议:
1. 由一个特定的问题生成一个类,然后在你解决其它问题的同时,让这个类自己生长,成熟。
2. 请记住,系统设计的主要工作就是探索发现你所需要的类(和它们的接口)。如果你已经拥有了这些类,完成这一项目就如探囊取物一般。
3. 不要试图在一开始就顾全所有的事情,要随着时间的推移不断学习。在什么情况下都是如此。
4. 开始编程,首先让程序能工作起来,此时你便可以证明你的设计是对是错。不要害怕你最后会得到面向过程风格的意大利面条式的代码——类将问题分割成小的部分,同时帮助你防止混乱,保持稳定。
5. 尽量保持对象的简洁性。小而清晰作用显著对象要远远好于庞大的繁杂的接口。当需要做出决策的时候,可以使用奥卡姆剃刀原则:考虑眼前可选的道路,选择其中最简单的一个,因为最简单的类通常都会是最好的。以一个小巧而简单的面貌开始,当你对其有了充分的了解后,就可以不断扩充该类的接口,但随着时间的推移,从一个类中移除一些元素就显得举步维艰。

第3阶段: 构建核心

这个阶段首次将大略的设计转向细节工作:编写可以编译,可以运行的代码,可以对这些代码进行测试,尤其是可以证实你的项目的架构是否合理。这并不是一个一次通过或者一次否决的过程,而是通过一系列步骤的迭代来构建系统,你将在第4阶段中看到详细信息。
当前的目标是寻找出你的系统架构的核心,这一核心是马上要实现的,这样系统才能运行起来,无论系统在一开始多么的不完整。你正在创建一个框架,而后你可以在这个框架基础上迭代构建。同时你也可以进行许多系统迭代和测试的初步工作,为投资商提供产品研发进程方面信息的反馈。理想状态下,你也可以向他们纰漏一些项目中存在的重大风险。这一阶段,你也许会发现你的原始构建中存在的一些亟待改进的地方——一些事情在你没有亲身经历之前,你是不会了解到的。
系统构建的一部分来自于对现实的检查,这一检查过程是通过对照测试结果与需求分析、系统说明书(无论它们以什么形式存在)之间的异同实现的。要确保测试能够验证需求和用例的有效性。当系统的核心稳定下来后,你已经可以在系统上添加各种新的功能了。

第4阶段:迭代用例

核心框架能够运行之后,你在系统中添加的每一个功能集合都可以看作一个独立的小工程。添加功能集合是通过迭代(iteration)实现的,迭代开发过程中占用很少的时间(这是理所当然的)。
迭代的规模有多大?理想状态下,每一次迭代需要一至三周(取决于所选用的语言)。在每次迭代的最后,你都会得到一个新的系统,它是整合的,经过测试的,并且拥有比以往更多的功能。这里存在一个尤为微妙的现象:每一次迭代对应着一个用例。每个用例中包含着你希望在一次迭代中添加入系统的相关功能。这不仅仅使你对一个用例的作用域应该有多宽有了更确切的认识,而且还更加坚定了使用用例的重要性,这是因为在分析和设计结束后,用例的理念也不会遭到丢弃,取而代之的是它将成为软件开发整个过程中一个基本的组成部分。
当你的作品的功能达到了预期设计目标,或者客户规定的截止日期就要到了,但他们对当前的版本已经很满意了,你就可以停止继续迭代。(不要忘记,软件开发是一个有投资的商业行为。)由于开发进程是可迭代的,你就拥有很多机会推出新产品,而不是等待唯一的一个目标。开源项目完全是迭代的,它拥有完善的反馈机制,借助于反馈机制,开源项目获得了令人瞩目的成果。
迭代的开发过程从很多意义上讲都是非常有价值的。你可以在早期发现并解决一些重大的风险,客户有了充足的机会更改他们的想法,程序员的热情也会更高涨,同时项目可以得到更精确的控制。但是一个更加重要的好处是,可以向投资商提供反馈,投资商就可以通过当前的产品的状态观察到各方面因素。这能够降低甚至消除那些令人昏昏欲睡的会议的数量,并且可以增强投资商的信心,使他们更加支持项目的进行。

第5阶段:进化

在开发周期中,这一过程的传统上称为“维护”,这一术语包罗万象,它可以表示“在初始阶段使得项目能以假设的模式工作起来”和“加入客户遗漏的功能”,以及一些更传统的内容,比如“修复显露出来的bug”和“在需求增加的时候添加新的功能”。诸如此类,常常被人们误认为是“维护”这一术语所指的内容,“维护”常常处于一个略显不合时宜的地位,一方面是因为它要求原始程序已经完成你所需要做的工作是对其进行部分的改变,添加“润滑油”,防止“生锈”。也许还有一些更恰当的词汇来描述所发生的事情。
我将使用“进化[6]”(evolution)这一术语。这就是说,“你在一开始得到的产品不会是完美的,所以你要学会随时做出改动。”随着你对问题的认识的逐步加深,你可能需要做出大量的修改。通过你的工作让产品进化是一件很优雅的事情,你会得到回报,无论是短期的还是长期的。进化使你的程序从优秀变得伟大,使你在一开始模糊的概念变得清晰。同样,你的类通过进化,可以由单一用途变成可复用的资源。
“正确运行”不仅仅指程序能正确地按照需求和用例运行起来,还要求内部代码结构要让人能够看得懂,并且要让人感觉到这一结构是合理的,没有古怪的语法,庞大的对象,或者拙劣的代码等等。另外,你必须意识到,程序结构将因经历各种改动得以保留,改动会贯穿于程序的整个生命周期中,这些改动很容易做到并且很简洁。这是不小的功绩。你不仅仅需要了解你当前正在构建的是什么,还需要知道程序将会如何进化(我所说的修改矢量[7] (vector of change))。索性的是,面向对象编程语言特别适合这种连续修改的工作——对象创建的边界是保证结构完整性的保障。他们也允许你做出修改——那些在过程语言中看上去十分重大的修改——这些修改将不会对你的代码带来地震般的破坏。实际上,支持进化也许是OOP最重要的好处所在。
通过进化,你可以创建一些与我们的想法相近似的程序,然后通过检查,将它与你的需求相比较,并且观察它是否会出错。然后你就可以返回,对不能正确工作的部分,通过重新设计和重新实现,对其进行改进[8]。实际上,在你得到正确的解决方案前的很长一段时间内,你可能需要解决这些问题,或者问题的某一方面。(第二卷中设计模式一章中的研讨对这一点也有帮助)
构建系统的过程中同样存在着进化,起初系统可能看上去满足你的需求,然而随着实践的发展你将发现它可能存在着这样那样的不尽人意的地方。当你看到系统已经投入运行,但是你却发现此时你正在尝试解决一个新的问题。如果你认为这种类型的进化会发生的话,你就以最快的速度完成系统的第一个版本吧,这样你就可以判断它是不是你真正想要的。
也许最重要的事情是:在默认状态下——其实也就是就其定义而言——如果你修改一个类,它的超类以及子类仍然会正常工作。你并不需要对于修改感到惧怕(尤其是如果你拥有一套内建单元测试系统来更正你的修改工作)。修改不意味着破坏程序本身,任何改动所带来的后果仅会影响到子类以及/或者特定的协同者。

做计划是值得的

当然,在没有一个成熟可行的计划之前,你不会去造一座房子。如果是盖一个屋顶或者狗舍,那么就没有必要做太过详细的计划,但仍需要一些草图来指引你的工作。软件开发显得更为极端。在很长一段时间内,人们的开发工作没有一个现成的结构可以借鉴,许多规模较大的项目都是以失败告终。方法论应运而生了,它包含大量的结构和细节,目标直指这些大项目。使用这些方法论是很可怕的——似乎你将所有的时间用在了书写文档,而无暇顾及代码。(事实往往就是如此。)我希望我在此介绍的内容可以帮你找到一条折衷的道路——可变的规模。使用一个最符合你的需求(和你的个性)的手段。要知道,大多数统计显示,超过百分之50的项目都失败了(一些统计数字显示是百分之70!)
通过遵循一个计划——越简洁明了越好——在编写代码之前完成结构草案,你将发现这样要比什么也不顾就埋头苦干来得轻松愉快的多,同时你也会得到强烈的满足感。这是我的个人感受,如果完成了一个优雅的解决方案,我将会得到完全不同层面上的满足,工作变得更像是一门艺术而不是技术。优雅的习惯总会得到回报的,这绝不是舍本逐末。这样你的程序不仅更加易于构建和排除错误,而且还更加易于理解和维护,这就是项目的经济价值所在。

[1] 感谢 James H Jarrett提供的帮助。

[2] 关于用例的更多信息请参见《用例分析技术》Schneider和Winters著 (Addison-Wesley 1998) 和《用例驱动UML对象建模》 Rosenberg 著(Addison-Wesley 1999).

[3] 近期我改变了这一看法。加倍和增加10% 给人的印象就是这一估计值是可以完全精确化的(假设这里并没有太多的wild-card因素),但是你仍然需要勤奋工作使得项目按期完成。如果你真的肯为完美地完成项目而不惜付出时间,并且能够在这一过程中享受到乐趣的话, 我相信,乘数将是3或4而不是2。

[4] 我向初学者推荐前面提到的《UML精髓》一书。

[5] Python (www.Python.org) 常常作为 “可执行的伪代码”。

[6] Martin Fowler的著作《重构:改善既有代码的设计》 (Addison-Wesley 1999)中详细介绍了若干个关于进化的例子。请注意这本书中的示例完全使用Java编写。

[7] 这一术语在本书第二卷中设计模式一章中研讨。

[8] 这看上去像“快速生成原型”,首先创建一个快速但简陋的版本,你就可以通过它来学习系统,然后你就可以丢弃这个原型去创建正式的版本。快速生成原型的问题是人们通常舍不得丢弃那些原型,而是在这些原型的基础上构建新的版本。与过程编程的结构的缺陷一样,这通常使得的系统的维护工作变得棘手并且昂贵。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: