寻找银弹

致力于探寻软件开发中的本质问题

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

第一部分:软件开发流程概要-《Head First OOA&D》读书笔记(1)

在第一部分中 (http://www.cnblogs.com/lihongchao/archive/2008/01/12/1036586.html), 简要的介绍了软件开发的一个典型过程,也就是所谓的生命周期. 要做好一个软件项目,这里的各个步骤都是必不可少的,并不是说这些步骤本身不可少,而是其中所 包含的具体工作肯定都会被考虑到,不管是有意的或者是无意的. 话说回来,要想能够出色的完成项目,光是盯住这些泛泛的步骤肯定是不行的,决定项目的成败关键在于我们在每一步所做的工作是否能够为下一步工作提供准确的,充分的信息和数据。 当我们做砸了一个项目回头来分析的时候(如果你还有这个机会的话J),你很有可能发现出问题的地方不是缺失了某一个步骤没有做,而是某一个步骤没有做得足够好,或者说是做错了方向。比如,需求说明文档描述的需求根本就不对,设计文档不合理,FixBug太草率,单元测试流于形式等等。记得看一本叫做《人件》的书上讲过,“没有计划好过于有个错误的计划”。一个错误的计划会麻痹真个团队,让所有人员认为这个计划是合理的,遵循这个错误的计划,以至于会一点点的把项目的开发引到向失败的结局。所以我们首先要确保我们所做的工作是基于正确的理解,朝着正确的方向在走,才有可能取得预期的结果。首先我们就讨论一下如何正确的走第一步,需求的收集与分析。

各个项目都做需求管理,最终的目标只有一个,就是用一种简洁,准确的方式表达出项目的真实需求。然后能够方便的向计算机语言转变这些需求。到底怎样才能达到这个艰巨的目标呢?不同的项目特点我们采取的手段也会不同。为了解决需求问题所采用的手段对整个开发过程都是关键的,最显著的就是项目所采用的开发流程是Waterfall 或者是Iterative, 再或者是SCRUM。其实需求的情况就是决定开发流程的一个关键因素。我们的目标就是开发满足用户期望的软件。因此我们需要知道用户的期望,那我们最开始需要做什么呢?

1. 愿景, 谈话, 功能列表(Vision, Discussion & Feature List

我们先不说我们需要先做什么,想一下,一个新项目是怎么开始的。大多数项目的开始都是很仓促的,领导或者是用户或者是其他的什么能指使你的人会给你一份一张纸的所谓需求文档,也有可能是上百张纸的文档,但其中有价值的内容一般也就一两张纸的样子,比书中的这个例子好不到哪里去。

clip_image002

Figure 1 愿景

这个东西是需求吗?也是也不是,只能说他是一个不完整的需求。其实这个只是用户的愿景,就是说只描述了一些支离破碎的激动人心的功能,而对于这些功能的使用细节和这些功能的联系却没有足够描述。 让我们按照这个来开发一个系统,肯定是无从下手。在我们把问题了解的充分清楚之前,要尽量减少后续工作。比如,在需求设计不清晰的时候,要尽量把开始写代码的时间拖后,因为你一旦开始构建系统,就说明你要再做任何修改都要花不小的代价了。那我们怎么做才能使需求清晰呢?交流!不光在项目开始的时候,在开发的全过程,客户的参与都是使项目成功的关键因素。但是在项目初期,交流的方式比较单一,一般就是拿着纸笔和客户面对面的交谈,尽可能详细记录客户的各种表达出的信息。对于不是很成熟的客户,我们很难把这种交谈深入下去,因为让用户凭空想象他们所需要的系统是什么样子确实不是一件容易的事情。有的时候,需要借助一些工具来启发客户,现有系统啊,存档的文档啊。一个常见的获取深层次需求的方法就是构造原型(Prototype),让用户有的放矢,这样能找出比较细致的需求。需要注意的一点就是,这个原型被构造的目的就是为了清晰我们的需求,没有其他任何作用。在需求告一段落的时候,原型就应该被抛弃,换句话说,原型就是一个最终要被废弃的产品。所以,我们开发原型的重点就是快速,准确的把我们理解的一些需求“实现”出来,供用户评判。我们开发原型的技术完全可以和以后开发系统的技术不同。比如现在很多公司都用Ruby来构造Web的原型,然后用DotNet或者Java开发系统。

如果有的需求比较抽象,不好描述,我们可以采取一些见解的手段,比如异同性分析(Commonality and variability,也就是描述,这个系统是什么,有那些类似的系统,功能类似,业务类似,甚至界面类似都可以。还要知道,不是哪样的系统,不包括什么功能,不需要考虑哪些因素,这样就把系统所需要涉及的范围(Scope)很好的定义出来了。

交谈结束后,我们要把得到的所有的需求再和用户确认,无误后存档,最好能够按照某种方式进行分类,以方便以后的查阅。这么多纷繁复杂的需求怎么去用?这就是个技巧问题了,其实在这个阶段,我们只需要把其中最重要的部分识别出来,所谓最重要,就是说如果我们的系统不实现这些功能那我们的系统就无法实现它的价值。我们要记住,要尽其所能把细节的问题退到最后再考虑,不然会使我们在整体的分析上遇到困难。其他的需求也是很重要的,但一般都在以后的工作中才能体现出价值,比如两个程序员关于功能实现意见不和的时候,我们就可以在这些需求记录中寻找相关记录,得到正确的方案。目前我们能够得到的是一个功能列表,能够按照优先级,依赖顺序排列,辅以简要介绍。这个功能列表要用客户熟悉的词语表示,然后确认这就是用户想要的东西。如果能把这些准确完成就算很成功的前期工作了。要注意一点,这里讲的功能(Feature)需求(Requirement)是没有明确的区别的,一般认为,功能更抽象一些,一般一个功能覆盖了,满足了多个需求,也可以把功能,这种更抽象的需求,依然叫做需求,像下图一样。

clip_image004

Figure 2 Feature List Sample

功能列表只是描述系统的一个角度,表示系统要具备哪些功能。另一个角度,就是用户能够怎样使用这个系统,就是我们常说的用例图(Use Case Diagram),这两种角度的文档相互对照,能够确保我们得到了完整的需求。

2. 实际场景(Real world)

我们的软件最终会在现实环境中使用,而不会永远运行在我们的开发和测试机器上,所做的操作也远远比我们的测试用例丰富的多。所以我们进行需求收集的时候,就要对于现实环境的一些情况要做足够的考虑。比如,不同用户的操作习惯, 用户的网络情况,用户的知识水平等等。很大程度上,我们是通过实际场景的想象和模拟来找出,我们需要处理的特殊情况,这些特殊情况出现的概率一般不高,但是对于系统的稳定性和客户的印象非常重要。只有预测并解决了在实际场景中可能遇到的情况,才真正的是站在客户角度,而不是开发者的角度,才能使用户满意。比如,在Gmail中,当你删除一封邮件以后,顶端会出现提示条。

clip_image006

Figure 3 Undo Option

让用户有机会能够撤销所作的操作,这样当有需要的时候,用户会非常高兴,他们会认为这个系统真正为他所想。如果没有这个功能,对于开发者完全可以把无法恢复的责任推给用户,是用户操作错误,进行了误删除。这种解释在相互的指责中可能会占上风,但是最终是没有满足用户真正想要的产品,这个需求。所以在需求收集,系统分析以及编码和测试的时候,都要考虑到,我们的产品最终会被安装到客户服务器上,会被真正的用户使用的。因此,我们要尽可能的把我们置身于用户的角度,在真正的实际场景中考虑我们的需求,设计以及开发的系统。这部分需求,一般客户比较容易忽略,因为他们认为是理所当然的,而对于开发人员确实闻所未闻的。

3. 问题分解和用例(Problem Break Up & Use Case

按照最终用户的分类,系统被使用的不同场景,以及系统的功能列表,我们可以把整个的系统进行逻辑上的模块拆分,把一个系统拆分成多个小的系统。这里的拆分仅仅是基于被使用的不同场景,拆分的目的是,使在每一个场景下,系统被使用的流程,以及用来实现的目标都比较单一,容易进行分析。

针对每一个系统被使用的场景,我们都要细致的进行对于需求的学习和理解,确保我们掌握了正确的需求。然后就可以详细的描述这种需求了。对于每一个用户的完整操作,都是一个相互独立的使用系统达到一个有价值目标的操作组合。比如,我需要用ATM机取款,我需要使用手机发短信。这些单独的应用场景就是用例(UseCase),也有叫做Story的,基本上是指的相同的东西。把系统能够满足用户的所有的这些有价值的操作组合全部整理出来,我们就得到了一个系统的用例列表,一般的讲,我们开发出的系统能够帮助用户实现这些目的,并且用户取得了预期的结果,那我们就算交付了一个成功的系统。这个用例列表的特点是,我们没有用任何技术词汇,通过文字和图形来表述需求,因此用户能够准确的理解它。这样,我们就可以和确定,用户所需要做的事情都在这个列表中涵盖了。更形象的表示方法是画出用例图(如下)。

clip_image008

Figure 4 用例图

用例图就是我们系统的一个蓝图(BluePrint),形象的描述我们的系统能为用户做什么事情。图中的小人就是指的用户(用户不一定是人,也有可能是其他的系统),图中的椭圆指的是,系统中需要实现的一个个用户的操作集合。我们就把这个椭圆叫做用例(Use Case)。用例描述了系统为了为用户实现的一个目标所做的一系列操作。一个完整的用例需要包含三个内容,一是用例要有清晰的价值。二是用例要有开始和结束点。三是,用例要由外部的实体触发开始。一个用例常常反映多种执行的顺序,通常有一个主要的执行路径和几个次要执行路径。次要执行路径只有在特定条件下才会使用。每一种执行的顺序,都叫做一个场景(Scenario). 这些场景都实现了一个相同的目标,也就是这个用例所体现的价值。我们使用用例的方式来描述需求的时候,一定要确保我们已经理解了需求,要多和用户交流,避免想当然的单独进行用例开发,而不和用户确认。用例可以用很多种方式描述,但目的都是要把用户使用系统的方法尽量表述全面,简洁。

文本描述,优点是易读,明了,但对于循环和逻辑分支的场景表述困难。

clip_image010

Figure 5 Use Case Sample

图表方法描述用例,可以非常清晰的描述系统操作的先后顺序以及各种条件判断。

clip_image012

Figure 6 Sequence Diagram Sample

当我们把所有需求都采用某种表现形式描述为用例的时候,我们就会发现一些隐藏的,我们没有注意的, 或者是用户没有告诉我们的需求。所以用例的一个价值就是帮助我们发现,管理用户的需求,而且很容易和用户就此进行交流。用例和功能列表也是有联系的,我们的用例必须覆盖所有的功能列表中记录的内容。这样,我们通过和客户交谈得到的功能列表就能够帮助我们来验证用例是否完整。实践中,有隐藏的用例没有被发现的情况是常常出现的。相反的,我们的功能列表中的功能不一定都能够找到一个对应的用例来使用这个功能,这是因为,用例描述这个系统如何被使用的,至于系统内部的调用关系,就没有清晰的表示了。由此可以看出,用例和功能是相互协调的,但是他们不是一回事。

有一种开发方式叫做场景驱动开发(Scenario Driven Developing)顾名思义就是一次锁定一个场景进行开发,使系统能够成功的执行这些操作。系统实现了这个场景以后,再挑选另一个场景进行实现,一直到所有的场景都实现后,开发工作也就结束了。也会被叫做用例驱动开发。可以看出场景驱动开发的着眼点是用户的需求,是实现一个个功能背后为用户解决的问题。这个出发点非常重要 ,不然的话,尽管实现了很多很酷的功能,界面也很漂亮,可到头来发现用户根本就不喜欢,也就没有什么价值了。

还有一种开发方式叫做功能驱动开发(Feature Driven Developing),也就是通过对功能列表中的功能进行细分(Break Down),然后挑选一个功能点进行开发,把系统中用到这个功能的地方都找出来,实现这个功能以满足所有潜在的要求。然后再进行另一个功能的实现,以此类推,实现了所有功能也就实现了整个系统。

这两种开发方法没有优劣之分,只有不同的适用场景。一般来说,场景驱动的颗粒度比较大,功能驱动的颗粒度比较小。也就是,一个系统分解成的场景数量要比分解成的功能数量少很多。如果拿软件开发比喻成画画,拿开发一个场景像是画一条完整的线段,不断的实现一个个场景,画面上的线条就越来越多。一个功能更像是一个点,不停的实现功能,画布上的点也就越来越多,最终形成了完整的画卷。

4. 领域分析(Domain Analysis)

有了完整的用例了,我们就可以进行领域分析了,也就是通过对现有系统,以及使用历史的研究,基于业务场景内的潜在知识,技术的了解,常常在领域专家的参与下,进行的相关知识,信息的收集,识别,表现的过程。因为这个过程重点是准确理解用户的目前业务逻辑,相关业务知识,所以非常需要用户的介入。因此,就需要用用户看得懂的语言来描述,我们的系统。经常见人说用户什么也不懂,一问原来,是把一些UML类图,模块划分,甚至代码给客户看,让客户确认这是他们需要的,这不是自讨苦吃吗。

分析的目的就是借助用户的知识来把系统进行模块划分,这个模块划分其实就是把系统由用户的角度转向开发者的角度的变化。用户只知道一个个的功能点,需求。而开发者要知道,我们的系统的模块,数据库怎么组织,业务逻辑如何封装,用户界面怎么表现,系统异常如何处理等。经过领域分析,我们基本就会确定一种系统架构的模式,是三层架构,MVC等等,甚至还会对一些具体功能实现有些想法,比如用哪个设计模式来解决等等。采用哪种系统架构,都是通过对这些需求,用例的分析而最后得出的,而不是一味的追捧潮流。话说回来,我们把系统细分成模块的目的就是,把一个复杂的系统分解成多个单独的简单的系统,一个个实现这些简单的系统,最后我们就会发现,这个复杂的系统就完成了。划分模块的原则和我们系统设计或者说是OO设计的原则类似,都是把相同属性,类似行为的需求,功能划分到同一模块中,尽量使模块之间的关系减少,减少模块之间的依赖关系。

对于一个个的小的模块,我们就可以比较轻松的使用面向对象的分析,设计方法来解决了。就拿图5的用例的例子来说,我们先找出来文字中的名词,这些名词都可以看成潜在的类或者叫做实体,对象。当然不能太僵化,有的名词不在系统中,比如Actor,有的名词不需要单独的类。再分析动词,这些动词都是主语所表示的实体类的潜在方法或属性。再运用一些OO的原则,对这个分析结果进行完善,把变化部分的隔离开,封装起来。把交互的部分,抽象成接口。最终就会生成一个重要的设计产品类图(Class Diagram),我将在下一篇中详细讲解这些OO的分析和设计的一些原则和方法。

clip_image014

Figure 7 Class Diagram

5. 迭代(Iteration

从上文能够看出来,我们通过对一个个较小的模块分别进行分析,设计,编码,测试,集成这几个步骤,最终完成整个系统的开发。我们可以吧不同的模块划分到几个迭代中进行开发,这样我们就能够在较短的时间内,完成一次迭代,有一个基本可以使用的系统供用户测试。确保我们的设计和实现是用户真正需要以后,再进行第二次迭代。和用户的设想有偏差的时候也好尽早进行沟通,对系统进行调整。这样就可以以较小的代价,逐渐的使需求清晰,满足用户。而那些模块放在第一次迭代,那些可以放到后边再实现,也是有讲究的。一般情况下,要着重考虑的就是哪些模块的风险最大,我们的理解最模糊,对整个系统的影响最大,我们就要首先解决这些模块。我们的目的就是不断的降低不确定性,减小开发的风险。在下文中我将详细介绍一下,怎样通过OO的分析来把这些充满不确性的需求逐渐转变成,能够操作的,具体的一个个类。

最后,留个作业,呵呵。讨论一下,采用这种迭代的开发方式,以及现在比较流行的敏捷开发,有哪些弊端?如何解决?提示一下,和用户相关。

posted on 2008-01-31 15:28  hchlee  阅读(2872)  评论(5编辑  收藏  举报