1.4 高质量软件开发的基本方法
1.4.1 建立软件过程规范
人们意识到,若想顺利开发出高质量的软件产品,必须有条理地组织技术开发活动和项目管理活动。我们把这些活动的组织形式称为过程模型。软件企业应当根据产品的特征,建立一整套在企业范围内通用的软件过程模型及规范,并形成制度,这样开发人员与管理人员就可以依照过程规范有条不紊地开展工作。
我们曾与国内很多研发人员和各级经理交流过,大家都对软件开发的混乱局面表示了不满和无奈。尽管“游击队”的开发模式到处可见,但是没有人真的喜欢混乱。“规范化”是区别“正规军”和“游击队”的根本标志,大家无不渴望以规范化的方式开发产品,这是现状,是需求,也是希望。
对软件开发模型的研究兴起于20世纪60年代末70年代初,典型成果是1970年提出的瀑布模型。人们研制了很多的软件开发模型,常见的还有“喷泉模型”、“增量模型”、“快速原型模型”、“螺旋模型”、“迭代模型”等。
这么多软件开发模型,企业应该如何选择并应用呢?
企业在选择软件开发模型时,不要太在乎学术上的“先进”与“落后”,正如有才华的人并不一定要出自名牌大学或拥有高学历那样。关键是看该模型能否有效地帮助企业顺利地开发出软件产品,并且要考虑员工们使用起来是否方便。简而言之,就是考察模型是否“实用、好用”。
最早出现的软件开发模型是瀑布模型,它太理想化、太单纯,看起来已经落后于现代的软件开发模式。如今瀑布模型几乎被学术界抛弃,偶尔被人提起,都属于被贬的对象,未留下一丝惋惜。说它如何如何地差,为的是说明新模型是怎样怎样地好。
然而企业界不同于学术界。我认为瀑布模型对企业太有价值了,我要为它声辩,恢复它应有的名誉。瀑布模型的精髓是“线性顺序”地开发软件。我们应该认识到“线性化”是人们最容易掌握并能熟练应用的思想方法。当人们碰到一个复杂的“非线性”问题时,总是千方百计地将其分解或转化为一系列简单的线性问题,然后逐个解决。一个软件系统的整体可能是复杂的,而细分后的子程序总是简单的,可以用“线性化”的方式来实现,否则干活就太累了。
让我们引用爱因斯坦的话作为信条——“任何事物都应该尽可能地简洁”。“线性”是一种简洁,简洁就是美。当我们领会了“线性”的精神,就不要再呆板地套用“线性”的外表,而应该用活它。例如,增量模型实质上就是分段的线性模型,螺旋模型则是迭代的弯曲了的线性模型。在其他模型中大都能够找到“线性”的影子。
瀑布模型是如此地简洁,所有的软件开发人员一学就会(如果学不会,那他就别干软件这一行了)。所以瀑布模型特别适合于企业,请大家别轻易地贬低它。
软件开发模型只关注技术开发活动,并不考虑项目管理,这对开发产品而言是不够的,所以开发模型只是软件过程模型的一部分。奇怪的是,迄今为止我尚未找到论述软件过程模型的软件工程书籍,我就自己创作了一个基于CMMI 3级的软件过程模型,称为“精简并行过程”(Simplified Parallel Process, SPP)。
SPP模型如图1-2所示。“精简并行过程”的含义是:
图1-2 精简并行过程(SPP)模型
(1)对CMMI 3级以内的过程域及关键实践做了“精简”处理。
(2)项目管理过程、技术开发过程和机构支撑过程“并行”开展。
SPP模型把产品生命周期划分为6个阶段:
✧ 产品概念阶段,记为PH0。
✧ 产品定义阶段,记为PH1。
✧ 产品开发阶段,记为PH2。
✧ 产品验证阶段,记为PH3。
✧ 用户验收阶段,记为PH4。
✧ 产品维护阶段,记为PH5。
在SPP模型中,一个项目从PH0到PH5共经历19个过程域(Process Area),它们被划分为3大类过程,如表1-2所示。其中项目管理过程含6个过程域,技术开发过程含8个过程域,支撑过程含5个过程域。
表1-2 SPP过程域分类
SPP模型的主要优点如下:
(1)模型直观。SPP模型是三层结构,上层是项目管理过程的集合,中层是技术开发过程的集合,下层是支撑过程的集合。这种模型很直观,高级经理、项目经理、开发人员、质量保证员等根据SPP模型就很容易知道自己“应该在什么时候做什么事情,以及按照什么规范去做事情”。SPP模型有助于使各个过程的活动有条不紊地开展。
(2)便于用户裁剪SPP模型。项目管理过程和支撑过程对绝大多数软件产品开发而言都是适用的。需求开发、技术预研、系统设计、编程、测试、技术评审、维护都是技术开发过程中必不可少的环节,用户可以根据产品的特征确定最合适的开发模型(如瀑布模型、快速原型模型、迭代模型等)。
(3)便于用户扩充SPP模型。如果产品同时涉及软件、硬件开发的话,可将产品生命周期、软件开发过程和硬件开发过程集成起来。
1.4.2 复用
复用就是指“利用现成的东西”。被复用的对象可以是有形的物体,也可以是无形的知识成果。复用不是人类懒惰的表现,而是智慧的表现。因为人类总是在继承了前人的成果,不断加以利用、改进或创新后才会进步。
复用有利于提高质量,提高生产率,并降低成本。由经验可知,通常在一个新系统中,大部分的内容是成熟的,只有小部分内容是创新的。一般可以相信成熟的东西总是比较可靠的(即具有高质量),而大量成熟的工作可以通过复用来快速实现(即具有高生产率)。勤劳并且聪明的人们应该把大部分的时间用在小比例的创新工作上,而把小部分的时间用在大比例的成熟工作中,这样才能把工作做得又快又好。
把复用的思想用于软件开发,称为软件复用。技术开发活动与管理活动中的任何成果都可以被复用,如思想方法、经验、程序、文档,等等。据统计,世上已有一千多亿行程序,无数功能被重写了成千上万次,真是浪费啊!面向对象(Object Oriented)学者的口头禅就是“请不要再发明相同的车轮子了”。
具有一定集成度并可以重复使用的软件组成单元称为软构件(Software Component)。软件复用可以表述为:构造新的软件系统可以不必每次从零做起,直接使用已有的软构件,即可在组装或加以合理修改后成为新的系统。
复用方法合理化并简化了软件开发过程,减少了总的开发工作量与维护代价,既降低了软件的成本,又提高了生产率。另一方面,由于软构件是经过反复使用验证的,自身具有较高的质量,因此由软构件组成的新系统也具有较高的质量。
软件复用不仅要使自己拿来方便,还要让别人拿去方便,是“拿来拿去主义”。这想法挺好,但现实中执行得并不如意。企业的业务各式各样,谁也不能坐等着天上掉下可以大规模复用的东西,一般要靠“日积月累”方能建设可以复用的软件库。从理论上讲,这项工作没有不可逾越的技术障碍。真正的障碍是它“耗时费钱”,前期投入较多,缺乏近期效益。大部分的公司都注重近期效益,不是它天生目光短浅,而是为了生存必须这么做。有些处境艰难的公司的下一顿饭还不知道着落,更别提软件复用这样的“长久之计”了。所以软件复用对大多数公司来说不是“最高优先级”。
我到公司工作的最初安排是从事电信领域“可复用软件工厂”的开发。领导在招聘时跟我讲,这个想法已经有数年了,一直没有落实,希望我能做好。待我正式上班时,马上就改成做其他短期的研发项目了。要知道我所在的公司人力与财力相当充足,非国内普通中小型IT企业所能比。即便我们有如此好的条件,软件复用也只是挂在嘴上,没有实际行动。
所以我建议:随时随地尽可能地复用你所能复用的东西,不要等待公司下达复用的行政命令,因为你很难等到那一天,即使等到了也没有多大的意义。
1.4.3 分而治之
分而治之是指把一个复杂的问题分解成若干个简单的问题,然后逐个解决。这种朴素的思想来源于人们的生活和工作经验,完全适合于技术领域。
分而治之说起来容易,做好却难,最糟糕的现象是“分是分了”,却“治不了”。
软件的分而治之不可以“硬分硬治”。不像为了吃一个西瓜或是一只鸡,挥刀斩成n块,再把每块塞进嘴里粉碎搅拌,然后交由胃肠来消化吸收,象征复杂问题的西瓜或是鸡也就此消失了。
软件的“分而治之”应该着重考虑:复杂问题分解后,每个问题能否用程序实现?所有程序能否最终集成为一个软件系统并有效解决原始的复杂问题?图1-3表示了软件的“分而治之”策略。软件的模块化设计就是分而治之的具体表现。
图1-3 软件的分而治之策略
1.4.4 优化与折中
软件的优化是指优化软件的各个质量属性,如提高运行速度、提高对内存资源的利用率、使用户界面更加友好、使三维图形的真实感更强,等等。想做好优化工作,首先要让开发人员都有正确的认识:优化工作不是可有可无的事情,而是必须要做的事情。
当优化工作成为一种责任时,开发人员才会不断改进软件中的数据结构、算法和程序,从而提高软件质量。
著名的3D游戏软件Quake,能够在PC上实时地绘制具有高度真实感的复杂场景。Quake的开发者能把很多成熟的图形技术发挥到极致,例如,把Bresenham画线、多边形裁剪、树遍历等算法的速度提高近一个数量级。我的博士研究方向是计算机图形学,我第一次看到Quake时不仅感到震动,而且深受打击。这个PC游戏软件的技术水平已经远胜于我所见识到的国内领先的图形学相关科研成果。这对国内日益盛行的“点到为止”的学术研究真是莫大的讽刺!
所以当我们开发出来的软件表现出很多不可救药的病症时,不要埋怨机器差。真的都是我们自己没有把工作做好。正所谓,写不好字不要嫌笔。
假设我们经过思想教育后,精神抖擞,随时准备为优化工作干上六天七夜时,要清醒地知道愿意做并不意味着就能把事情做好。优化工作的复杂之处是很多目标之间存在千丝万 的关系,可谓“剪不断理还乱”。当不能够使所有的目标都得到优化时,就需要“折中”。
软件中的“折中”是指协调各个质量属性,实现整体质量的最优,正如“……为了使整个组织具有最好的战斗力,我们要重用几个人,照顾一些人,在万不得已的情况下委屈一批人”。
软件折中的重要原则是不能使某一方损失关键的功能,更不可以像“舍鱼而取熊掌”那样抛弃一方。例如,3D动画软件的瓶颈通常是速度,但如果为了提高速度而在程序中取消光照明计算,那么场景就会丧失真实感,3D动画也就不再有意义了。
人都有惰性,如果允许滥用折中的话,那么一旦碰到困难,人们就会用“拆东墙补西墙”的方式去折中,不再下苦功去做有意义的优化。所以我们有必要为折中制定严正的立场:在保证其他质量属性不差的前提下,使某些重要质量属性变得更好。
下面让我们用优化与折中的思想解决“鱼与熊掌不可得兼”的难题。
问题提出:假设鱼每千克10元,熊掌每千克10000元。有个倔脾气的人只有20元钱,非得要吃上一公斤美妙的“熊掌烧鱼”,怎么办?
解决方案:花9元9角9分钱买999克鱼肉,花10元钱买1克熊掌肉,可做一道“熊掌烧鱼”菜,剩下的那一分钱还可建立基金,用于推广优化与折中方法。
1.4.5 技术评审
技术评审(Technical Review, TR)的目的是尽早地发现工作成果中的缺陷,并帮助开发人员及时消除缺陷,从而有效地提高产品的质量。
技术评审最初是由IBM公司为了提高软件质量和提高程序员生产率而倡导的。技术评审方法已经被业界广泛采用并收到了很好的效果,它被普遍认为是软件开发的最佳实践之一。技术评审能够在任何开发阶段执行,它可以比测试更早地发现并消除工作成果中的缺陷。技术评审的主要好处有:
✧ 通过消除工作成果的缺陷而提高产品的质量。
✧ 越早消除缺陷就越能降低开发成本。
✧ 开发人员能够及时地得到同行专家的帮助和指导,无疑会加深对工作成果的理解,更好地预防缺陷,在一定程度上能提高开发效率。
技术评审有以下两种基本类型。
✧ 正式技术评审(FTR):FTR比较严格,需要举行评审会议,参加评审会议的人员比较多。
✧ 非正式技术评审(ITR):ITR的形式比较灵活,通常在同伴之间开展,不必举行评审会议,评审人员比较少。
从理论上讲,为了确保产品的质量,产品的所有工作成果都应当接受技术评审。现实中为了节约时间,允许人们有选择地对工作成果进行技术评审。技术评审方式也视工作成果的重要性和复杂性而定。例如,将重要性、复杂性各分“高、中、低”3个等级,重要性—复杂性的组合与技术评审方式的对应关系如表1-3所示。
表1-3 工作成果重要性—复杂性组合与技术评审方式的对应关系
正式技术评审的一般流程如图1-4所示。
图1-4 正式技术评审的一般流程
技术评审的注意事项:
✧ 评审人员的职责是发现工作成果中的缺陷,并帮助开发人员找到消除缺陷的办法,而不是替开发人员消除缺陷。
✧ 技术评审应当“就事论事”,不要打击有失误的开发人员的工作积极性,更不能搞人身攻击(如挖苦、讽刺等)。
✧ 在会议评审期间要限制过多的争论,以免浪费他人的时间。
对技术评审的一些建议:
✧ 对于重要性和复杂性都很高的工作成果,建议先在项目内部进行“非正式技术评审”,然后再进行“正式技术评审”。
✧ 技术评审应当与质量保证有机地结合起来,最好请质量保证人员参加并监正式技术评审。
✧ 技术评审应当与配置管理有机地结合起来,如规定:工作成果在成为基准(Baseline)之前必须先通过技术评审。
✧ 建议机构采用统一的缺陷跟踪工具,使得技术评审所发现的缺陷能够及时地消除,不致遗漏。
1.4.6 测试
测试是通过运行测试用例(Test Case)来找出软件中的缺陷。测试与技术评审的主要区别是:前者要运行软件而后者不必运行软件(动态检查和静态检查)。
在软件开发过程中,编程和测试是紧密相关的技术活动,缺一不可。理论上讲两者不分贵,同等重要。但在大多数软件企业中,程序员的待遇普遍要高于专职的测试人员。即使不考虑待遇问题,大多数人认为开发工作比测试工作更有趣、更有成就感、更有前途。所以计算机专业人员通常会把编程当成一种看家本领,舍得下功夫学习和钻研,但极少有人以这种态度对待软件测试。这种意识导致软件测试被过于轻视。不仅学生们在读书时懒得学习测试(目前国内高校似乎没有“软件测试”的课程),就连有数年工作经验的软件开发人员也未必懂得测试。
不少开发人员虽然不敌视测试工作,但有“临时抱佛脚”的坏习惯,往往事到临头才到处找测试资料,向人请教。经常有人向我要文档模板,用来对付测试。
你以为有了文档模板就懂得如何测试了么?
测试虽然并不深奥,但是学好并不容易。不懂得“有效”测试的项目小组往往面临这样的问题:计划中的时间很快就用完了,即使有迹象表明软件中还有不少缺陷,也只好草草收场,把麻烦留给将来。
测试的主要目的是为了发现尽可能多的缺陷。可是这个观念不容易被人接受。
正确理解测试的目的十分重要。如果认为测试的目的是为了说明软件中没有缺陷,那么测试人员就会向这个目标靠拢,因而下意识地选用一些不易暴露错误的测试示例。这样的测试是不真实的。如果是为了说明软件有多么好,那么应当制作专门的演示。千万不要将“测试”与“演示”混为一谈。
看来测试并不单单是个技术问题,还是个职业道德问题。
根据测试的目的,可以得出一个推论:成功的测试在于发现了迄今尚未发现的缺陷。所以测试人员的职责是设计出这样的测试用例,它能有效地揭示潜伏在软件里的缺陷。
如果测试工作很全面、很认真,但是的确没有发现新的缺陷,那么这样的测试是否毫无价值?
不,那不是测试的过失,应当反过来理解:软件的质量实在是太好了,以至于这样的测试发现不了缺陷。
所以,如果产品通过了严格的测试,大家不要不 气,应当好好地宣传一下,把测试的成本 回一些。
软件测试的常规分类如表1-4所示,测试的一般流程如图1-5所示。如果对表1-4和图1-5的内容做深入论述,大约还需要几十页的篇幅,这显然超出了本章的范畴。这里就点到为止吧,请读者参考软件测试的有关文献。
表1-4 测试的常规分类
图1-5 测试的一般流程
测试经验谈:
✧ 测试能提高软件的质量,但是提高质量不能依赖测试。
✧ 测试只能证明缺陷存在,不能证明缺陷不存在。“彻底地测试”难以成为现实,要考虑时间、费用等限制,不允许无休止地测试。我们只好祈祷:软件的缺陷在产品被淘汰之前一直没有机会发作!
✧ 测试的主要困难是不知道如何进行有效的测试,也不知道什么时候可以放心地结束测试。
✧ 每个开发人员应当测试自己的程序(分内之事),但是不能作为该程序已经通过测试的依据(所以项目才需要独立的测试人员)。
✧ 2-8原则:80的缺陷聚集在20的模块中,经常出错的模块改错后还会经常出错。
1.4.7 质量保证
质量保证(Quality Assurance, QA)的目的是提供一种有效的人员组织形式和管理方法,通过客观地检查和监控“过程质量”与“产品质量”,从而实现持续地改进质量。质量保证是一种有计划的、贯穿于整个产品生命周期的质量管理方法。
过程质量与产品质量存在某种因果关系,通常“好的过程”产生“好的产品”,而“差的过程”将产生“差的产品”。人们销售的是产品而不是过程,用户关心的是最终产品的质量,而软件开发团队既要关心过程质量又要关心产品质量。
质量保证的基本方法是通过有计划地检查“工作过程及工作成果”是否符合既定的规范,来监控和改进“过程质量”与“产品质量”。如果“工作过程及工作成果”不符合既定的规范,那么产品的质量肯定有问题。基于这样的推理,质量保证人员即使不是技术专家,他也能够客观地检查和监控产品的质量,这是质量保证方法富有成效的一面。但是“工作过程及工作成果”符合既定的规范并不意味着产品的质量一定合格,因为仅靠规范无法识别出产品中可能存在的大量缺陷,这是质量保证方法的不足之处。所以单独的“质量保证”其实并不能保证质量。
技术评审与测试关注的是产品质量而不是过程质量,两者的技术强度比质量保证要高得多。技术评审和测试能弥补质量保证的不足,三者是相辅相成的质量管理方法。我们在实践中不能将质量保证、技术评审和测试混为一谈,也不能把三者孤立起来执行。建议让质量保证人员参加并监督重要的技术评审和测试工作,把三者有机地结合起来,才能提高工作效率和降低成本。
质量保证小组(Quality Assurance Group, QAG)有如下特点:
✧ 质量保证小组在行政上独立于任何项目。这种独立性有助于质量保证小组客观地检查和监控产品的质量。
✧ 质量保证小组有一定的权利,可以对质量不合格的工作成果做出处理。这种权利使得质量保证小组的工作不会被轻视,并有助于加强全员的质量意识。需要强调的是,提高产品质量是全员的职责,并非只是质量保证小组的职责。
质量保证过程域的主要活动如图1-6所示。
图1-6 质量保证过程域示意图
1.4.8 改错
改错是个大悲大喜的过程,一天之内可以让人在悲伤的低 和喜悦的巅峰之间荡起伏。
我从大三开始真正接受改错的磨炼,已记不清楚多少次汗流浃背、湿透板凳。改不了错误时,恨不得撞墙。改了错误时,比女孩子朝我笑笑还开心。
在做本科毕业设计时,一天夜里,一哥们儿来我的实验室,合不拢嘴地对我嚷嚷:“你知道什么叫茅塞顿开吗?”
我像文盲似地摇摇头。
他说:“今天我花了十几小时没能干掉一个错误,刚才我去了厕所五分钟,一切都解决了。”
软件中的错误通常只有开发者自己才能找出并改掉,如果因 惧而拖延,会让你终日心神不定,食无味,睡不香。所以长痛不如短痛,要集中精力对付错误。
改错过程很像 破案件,有些坏事发生了,而仅有的信息就是它的确发生了。我们必须从结果出发,逆向思考。改错的第一步是找出错误的根源,如同医生治病,必须先找出病因才能“对症下药”。
有人问阿凡提:“我肚子痛,应该用什么药?”
阿凡提说:“应该用眼药水,因为你眼睛不好,吃了脏东西才肚子痛。”
根据软件错误的症状推断出根源并不是件容易的事儿,因为:
(1)症状和根源可能相隔很远。也就是说,症状可能在某一个程序单元中出现,而根源实际上在很远的另一个地方。高度耦合的程序结构加剧了这种情况。
(2)症状可能在另一个错误被纠正后暂时消失。
(3)症状可能并不是由某个程序错误直接引发的,如误差累积。
(4)症状可能是由不太容易跟踪的人工错误引起的。
(5)症状可能时隐时现,如内存泄漏。
(6)很难重新产生完全一样的输入条件,难以重现“错误现场”。
(7)症状可能分布在许多不同的任务中,难以跟踪。
改错的最大忌 是“急躁 干”。人们常说“急中生智”,我不信,我认为大多数人着了急就会 干,早把“智”丢到脑后去了。不仅人如此,动物也如此。
我们经常看到,蜜蜂或者苍蝇想从玻璃窗中飞出,它们会顶着玻璃折腾几个小时,却不晓得从旁边轻轻松松地飞走。我原以为蜜蜂和苍蝇长得太小,视野有限,以至看不见近在咫尺的逃生之窗,所以只好蛮干。可是有一天夜里,有只麻雀飞进我的房间,它的逃生方式竟然与蜜蜂一模一样。我用灯光照着那扇打开的窗户为其引路,并向它打手势,对它说话,均无济于事。它是到天亮后才飞走的,这一宿我和它都没有休息好。
我们把寻找错误根源的过程称为调试(Debugging)。调试的基本方法是“粗分细找”。对于隐藏得很深的Bug,我们应该运用归纳、推理、“二分”等方法先“快速、粗略”地确定错误根源的范围,然后再用调试工具仔细地跟踪此范围的源代码。如果没有调试工具,那么只好用“土办法”:在程序中插入打印语句,如printf(…),观看屏幕的输出。
有些时候,世界上最好的调试工具恐怕是那些有经验的人。我们经常会长时间地追踪某个Bug,苦恼万分。恰好有高手路过,被他一语“道破天机”,顿时沮丧的云就被驱散,你不得不说“I服了You”。
修改代码错误时的注意事项
✧ 找到错误时,不要急于修改,先思考一下:修改此代码会不会引发其他问题?如果没有问题,则可以放心修改;如果有问题,那么可能要改动程序结构,而不止一行代码。
✧ 有些时候,软件中可能潜伏同一类型的许多错误(如由不良的编程习惯引起的),好不容易逮住一个,应当乘胜追击,全部歼灭。
✧ 在改错之后一定要马上进行回归测试,以免引入新的错误。有人在马路上捡到钱包后得意忘形,不料自己却被汽车 倒。改了一个程序错误固然是喜事,但要防止乐极生悲。更加严格的要求是:不论原有程序是否绝对正确,只要对此程序做过改动(哪怕是微不足道的),都要进行回归测试。
✧ 上述事情做完后,应当好好反思:我为什么会犯这样的错误?怎么能够防止下次不犯相似的错误?最好能写一下心得体会,与他人共享经验教训。