【IT168 技术文章】
意图
无论哪一种过程,其最终目的都是为了产生出可执行、并且可用的软件。因此软件过程中的各种活动应该围绕着快速、准确的实现这一目的而展开的。
示例
维力亚软件公司是一家合资公司,由于有外资背景,公司内部很早就引入了软件工程,并严格的对人员角色进行分工。包括领域建模人员、架构设计师、高级程序员、程序员、界面设计师等等多种角色。每个人各司其职,充分发挥出了分工的特点。但是随着公司开发项目的逐渐增多,这种方式也显露出其弊端来。每个人的主要目标都是为了通过评审,而有时候,就算是通过评审的工件,依然可能存在问题。但这时候扯皮就出现了。项目中存在的一些中空地带。以及交错地带,常常发生无人问津的情况。开发过程的效率开始下降,开发成本开始上升。问题虽然不是一下子出现的,但是已经逐渐变得严重起来了。
上下文
我们在进行过程设计,或引入一个过程理论的时候,有没有思考过该过程的每一个阶段、每一个活动的目的是什么,它们对生成最后的软件有什么样的帮助,这些帮助对于我们所在的组织有意义吗。很多情况下,我们并没有这么做,或者随着软件过程的定型,就不再思考这类的问题。一开始并没有什么了不起的,但是当软件过程演变成了一种政治体系的时候,那么问题就会慢慢严重起来。
问题
如何让过程围绕着产出软件的核心目标而不断演进?
方法
从上一篇介绍的内容中,我们知道软件过程的每一个阶段都是知识转换的过程,知识转换的终点就是软件。问题在于,我们如何保证这种转换的效率呢?
现代软件的发展的趋势是重用。我们开发一个软件已经很少会从最底层开始编写了。我们使用各种各样的技术和平台。包括数据库、分布式体系、UI机制、业务元素等等。因此现在的软件编写往往都是站在巨人的肩膀上开始的。不同的软件组织,肩膀的高低也不一样。比如有的软件组织使用J2EE平台,有的软件组织则使用.NET平台。但是可以肯定的一点是,每个软件组织都或多或少的拥有自己的平台体系开发经验。这些经验的表现形式也不尽相同,可能是保存在某些高级技术人员的脑中,也可能是保存为文档、模型或代码的形式。
对于单个的项目而言,其过程一定是从需求开始,以部署(以及后续维护)为终结的。但是对于长时间存在的软件组织而言,更重要的是项目经验、技术经验、管理经验的积累。这样说可能过于抽象,我们举一个具体的例子。在完成了一个库存管理项目之后,我们把库存管理的领域知识制作为商业组件的形式,而把项目中学习到的一些编码的技巧整理为模式的形式,并把项目过程中积累的过程方法添加到现有的软件过程中。这些经验的堆积就是在一开始我们提出的可重用框架。对一个软件团队的来说,它的所有软件项目都应该是围绕着这一个可重用框架而展开的。
迄今为止,我们见过的大多数的可重用框架表现形式都可以总结为:以开发平台为基础,积累自己的商业组件,并以此为中心订立开发活动。这种形式是不是最好并没有定论,但我们是抱着学习的态度来研究它的。
以开发平台为基础的意思是软件组织必须有自己所熟悉的开发平台,其大部分的项目都是在此基础上进行的。目前最流行的两种软件开发平台是J2EE和.NET,但是就算是同一个平台,不同的软件组织对平台内部的侧重也是不同的(同样对于J2EE技术,有的软件组织可能把JSP和Servlet作为核心选择,而另一些软件组织则把重点放在EJB上)。选择一种广泛应用的、具有生命力的平台技术是非常重要的。因为你的框架将会以此为基础进行,而框架的转移成本是非常之高的。虽然我们在一开始介绍的MDA思路为我们绘制了一副平台无关设计的美好愿景,但是在目前阶段,我们仍然要面对不同软件平台造成的严重隔阂。
必须指出的是,我们上面提到的开发平台指的是在操作系统这个层次之上的平台,也就是俗称的中间件平台。但是从中间件到最终的产品之间有没有过渡的平台呢。其实可重用框架就扮演了这一角色。软件市场上已经出现了商业化的可重用框架了。IBM的SanFrancisco框架就是这种概念的代表。
平台技术仅仅只是提供了一个技术,而软件组织要生存和发展,还需要积累和发展自己的商业组件。并将商业组件发展成为可重用框架。商业组件的好坏,直接和软件组织的能力、经验,以及平台技术相关。商业组件可能直接表现为代码的形式(Bean、类、COM组件等),也可能只是描述性的记录(文档)。商业组件是经验积累而成的。请注意,商业组件的设计并不完全等同于面向对象开发,因为面向对象只是一种技术手段,但是商业组件设计的优劣,更重要的是对业务的理解程度。举一个最浅显的例子,一个精通面向对象开发、面向组件开发的设计师,让他介入他完全不了解的行业组件设计,他的表现将会大打折扣。
到目前为之,大家所认识的框架仍然是技术型的框架。但事实并非如此,框架还包括了外延的一系列软件活动。这才是一个完整的框架。结合之前我们讨论的软件交付为开发目标。我们把这种开发方式称为以可重用框架为核心,以交付为目标的软件开发。这其实并不是什么了不起的概念,大部分的软件组织都已经这么做了,只是没有意识到自己的方式而已。了解这一点,能够帮助软件组织有效的改进自身的构架。
平台技术和组件开发并不是本文的重点,因此我们在肯定了两者的重要性之后,把精力转移到软件活动上。
在拥有一个框架核心(平台和商业组件)之后,框架需要包含这样的活动(或活动集):收集项目需求,并将需求映射到核心构架上来。这其实就是需求阶段到设计阶段要做的事情。但是由于我们的活动是以软件交付为目标的,因此我们需要明确的指出活动中的注意事项。
为映射工作设计需求描述规格。需求并不是一件容易的事。最难的莫过于尺度的把握了,例如需求要多详细。使用现成的技术来定义需求描述规格,并根据核心框架的特点进行必要的扩展。例如,我们使用成熟的用例技术来描述需求,同时我们要求需求按照不同类的商业组件进行分类索引。用例技术的推荐读物是Alistair Cockburn的Writing Effective Use Cases一书,该书目前已有英文影印版。
保证需求规格能够被项目成员所理解。这里的项目成员包括客户、领域专家、需求调研者、分析模型设计师。只有他们了解需求,才能够保证信息的正确的传递。(参见 知识接力模式)。
为实现需求制定分析(设计)规则和指南 。这是把需求映射到核心构架上的重要步骤。制定规则是必要的,但要小心,不要让规则限制住开发人员的创造力(参见 活跃和混乱、严谨和死板模式)。规则的形式可能是设计规范、分析模式、类库、组件重用等等。在指南中提供示例,描述如何将需求转换为设计模型是一种不错的做法。同样好的做法还包括了模式指南。
确保测试贯穿了需求模型和设计模型。我们终于提到了测试。测试在软件过程中扮演着重要的角色。但遗憾的是在本文中直接提到的机会并不多,从某个角度上看。 知识接力模式中提到的复审其实也算是一种测试。测试的信息都包含在需求模型和设计模型之中,例如前置条件和后置条件。在完成需求模型和设计模型时同步完成测试用例是一种非常好的做法(我们的团队正是采用这种做法),但是需要小心文档一致性(参见 一致性的思考模式)的所需要付出的额外成本。
如同在 知识接力模式中提到的那样,让领域专家、架构师和高级开发人员对需求模型和设计模型进行复审。
原型方法能够有效的帮助最终软件的成功。所谓原型方法,就是选取系统的某个部分(最直接或风险最高的部分,通常是界面原型), 实现并呈现给用户,以获得反馈,为后续的活动提供指导。原型方法最大的好处是能够帮助用户认识软件,消除用户的疑虑,并发掘潜藏的需求。围绕着是否抛弃原型这一根本问题,原型方法可以分为渐进原型方法和舍弃型原型方法。前者是在一个软件原型的基础上不断的演进,并最终发展为可用的软件,后者则是在开发出原型之后就将它舍弃。渐进原型方法充分利用了原型,但是由于缺乏前期设计,可能会导致最终产品存在性能或设计问题。舍弃型原型克服了这个问题,但是它浪费了原型开发的那段时间。不论采用何种方法,最重要的是在项目一开始就决定采用哪一种原型方法。模棱两可的使用两种方法是兵家大忌。最终你无法利用任何一种方法的优点,而所有的缺点都将降临到你身上。相较而言,渐进型原型方法更适合于应用在小型项目上,因为项目并不复杂的话,设计的改进比较容易。对于一个拥有构架的团队而言,把原型方法纳入构架之中是很有意义的。如果构架足够成数,迅速开发出一个原型并不是什么很困难的事情。这样就可以在投入最小化的情况下获得原型方法的优势。如果情况是这样的话,舍弃型原型方法似乎更适合一些。
在知识接力模式中,我们简要的提到了设计模型信息和代码信息的转换问题。使用建模工具来自动从设计模型抽取信息,并生成项目的代码。这种方法能够大幅度的提高软件开发效率,并对软件的最终交付有很大的帮助。同样,将代码中的信息转回到设计模型中(属于反向工程的一部分)也是有意义的。如果缺少这样的工具,那么请人为的保证信息的同步,当然,并没有必要保持实时的同步(参见 一致性的思考模式)。
软件的成功和测试活动是无法区分的。我们前面简单的讨论过测试信息是来源于需求的。测试信息随着需求模型的生成而生成,并通过设计模型进行转换,在软件过程进入到实现阶段时,测试信息最终被转变为单元测试用例的形式。单元测试用例可能是针对单个方法的单个用例,也可能是针对某个开发包的几组用例。我们需要注意两点,首先是在软件过程中保证这个流程的顺畅和正确。就像在 知识接力模式中讨论的那样,正确、完整的信息传递保证了最终软件的成功出产,测试信息的成功传递保证了最终软件的可用性。测试是软件的保证。因此,我们需要几个活动来保证测试信息的成功:
从需求模型中生成接受测试。该活动把需求映射到测试上。在这个活动中,不但要注意功能性需求(如完成的功能),还需要注意非功能性需求(性能要求)。同样的,接受测试也需要接受复审。可以按照需求的组织方式来组织接受测试。
设计模型完成之后,接受测试已经细化到模型的各个元素上了(例如包、类)。该项活动和将需求映射到设计的活动是同步进行的。因为它们处理的信息是非常类似的。和接受测试一样,这两个活动都需要由团队来保证。
在进入编码阶段之后,开发人员根据接受测试和设计模型,将会为自己负责的部分设计编写单元测试。单元测试的编写顺序是否优先于编码,这取决于各人的看法。关于这方面的讨论,可以参看XP的单元测试实践。从我个人的经验来看,先写单元测试,再写代码不失为一个很好的办法,另一种做法是,让两个紧密合作的开发人员相互为对方编写单元测试。
其次,我们需要保证测试的最小单元-单元测试的成功。所谓皮之不存,毛将焉附。单元测试没有组织好,最后的软件是不会成功的:
需要注意单元测试的覆盖率。这里的覆盖率指的是是否能够完全检测出错误所在。比如边界条件的测试等。开发团队中的架构师之类的角色需要为此负责,如果无法检查所有的单元测试,那么可以进行抽查和代码复审会议的形式。后者是一种很优秀的做法。从代码中抽取出一段,开发人员一起分析单元测试存在哪些问题,会使得大家编写单元测试的功力不断的增长。
单元测试一定要是自动化的。Junit就是一个不错的自动化测试框架。保证单元测试的自动化,并且避免单元测试和特定环境相关,这样就可以顺利的进行回归测试。这对于迭代式的软件开发是非常必要的。同时,我们也应该认识到,单元测试的稳定性取决于设计的稳定性。我们可以想象,如果测试的类方法的参数、命名都发生改变,那么测试是肯定需要重新组织的。所以,面向对象的抽象思维对于现代软件开发而言是费产重要的。为了做到测试的自动化,尽量避免将逻辑硬编码在界面中,并小心的处理数据库。可以尝试着建立测试数据库。
如果测试的内容需要并入核心构架,那么这部分的测试工作需要增加,并对构架可能的修改进行测试。因此,学习开源软件的做法,将构架的稳定版本和开发版本相区分是比较保险的。
让测试活动随着软件开发的进行而进行,让测试活动贯穿整个开发周期。这有点类似于全面质量管理的思想。因为软件开发过程的失败并不是突然而至的,而是在平时的不经意间一点一点的积累起来的。使用测试活动来消除这种微小的不稳定性,能够大幅度的提高最终软件的质量。但是,在一个团队中引入正规的。频繁的测试活动是需要耐心的。可以先从单元测试着手,慢慢的普及这种做法。
测试活动可以衍生为日创建和冒烟测试。对于有复数的开发人员参与的软件项目,就无法避免正视集成测试的局面。有过类似经验的人都可以想象的到这种集成测试的难度。专门有一句话是描述这种局面的:"我们已经花了90%的时间来完成90%的软件,但我们还需要90%的时间来完成剩下的10%"。现代的软件往往都包括成百上千的文件,这些文件的编译链接的过程是很复杂的。而日创建的思想就是每天进行一次这样的过程,形成一个可执行文件。但这只是日创建第一部分内容。日创建还需要对软件进行冒烟测试,测试的主要内容就是单元测试中所编写的内容,保证软件可以通过所有的测试。
日创建需要留下一些调试的时间,尤其是在软件开发初期,不同开发人员的代码整合在一起出现问题的可能性极大。随着对这项实践的熟悉程度,问题会逐渐减少。日创建可以很明显的提高产品质量,以及提升团队的士气。虽然日创建活动需要付出额外的一些成本,但是相对于集成测试的不可控,这些成本还是值得的。
小结
投资于框架能够保证软件开发的成功。
应用有效的实践活动来完善框架。
将需求映射到核心框架上来。
使用原型法。
注意测试活动。
如果可能,使用日创建,并进行冒烟测试。
规则不同于指南。规则是目标受众所必须遵守的,而指南是建议目标受众遵守的。