软件工程过程
软件工程需要处理的要素,大体上可以列举如下:
市场需求

系统级别的设计
详细设计

实现
集成
区域性测试
支持
在上述过程的组成部分中,后一项的工作不应该在前一项工作尚未结束前就开始,并且无论哪一组成部分作了变化,其余相关的部分都应该依照此项变动进行再检查或重新实现。一个模块很有可能在其所依赖的模块未被完整详细说明前就已被详细说明并被实现。此种方能称之为“高级开发或研究”(advanced development or research)。
在软件工程过程的每个组成部分中,有几种检查(review)是绝对必要的:同侪检查(peer review),导师/管理人员检查(mentor/management review)和交叉规律性检查(cross-disciplinary review)。
软件工程组成部分(无论是软件文档,还是源代码),必须含有版本号和审计历史的记录。修改时对每一个要素的“checking in”都需要某种形式的检查,检查的深度与修改变动的范围直接相关。
市场需求
软件工程过程的第一步是建立一个文档,来描述顾客的目标要求和他们需要此产品的原因,并且列出该产品的特性以表明顾客的需求。市场需求文档(Marketing Requirements Documents,MRD)就是用以回答“我们要做什么、谁会使用它”的文档。
在许多失败的项目中,MRD就如同是从市场交付到工程的一篇刻在石碑上的文章,从MRD开始,对工程的每一个细节进行详细的分析,就可以检查出项目失败的各种原因。完成MRD需要综合各方面的努力,这一过程不仅需要检查,而且的的确确需要编写许多文字。
系统级别的设计
这是一个对产品更高级别的描述。以“模块”(有时也称“程序”)为单位,描述模块和模块之间的接口进行描述。这份文档之目的在于,使开发人员首先能得到更多的自信、以保证产品能运行,并动手去后续的开发。其次,系统级别的设计应该形成一个评测基础,以估计所有的开发工作量。
系统级别的设计文档还应包含系统级别测试计划的概要介绍,该测试计划应依照用户需求作出,而且要评价系统级别的设计本身是否满足用户的需要。
详细设计
详细设计,是指对在系统级别设计文档中描述的需要被调用每个模块进行的详细设计,每个模块的接口(在命令行格式下称为API,从外部可见的数据结构)必须在文档中被确定,同样也包括模块之间的侬赖关系。在详细设计之外还有两件事要做:用PERT或GANT显示要做什么工作,以及这些工作的顺序和每个模块完成的具体时间。
每个模块需要有一个单元测试计划,该计划可以让实施者知道什么样的测试实例,以及哪种测试实例需要在他们的单元测试中产生,以验证模块的功能。注意:另一点是非功能化单元测试,我们将稍后讨论。
实施
在详细设计文档中描述的每个模块都必须被实现。这包括“编码”(coding)或“编程”(programming)中细小的动作,这些细小的动作正是软件工程过程的核心和灵魂所在。但不幸的是,这个细小的动作只是有时被当作软件工程教学的一部分内容,从而只是被当作程序员高效率地自学软件工程时的一部分内容。
一个模块在创建、测试并与其他模块(或被系统级别的测试进程)协同调用后方可视作已完成。建立一个模块是一种老式的“编辑-编译-重复”的循环过程。模块测试包括单元级别功能和详细设计中调用的逆向测试,并且也进行性能/重点测试(performance/stress testing)和涉及代码的分析。
集成
当所有的模块都完成后,系统级别的集成就可以开始了,也就是将所有的模块代码集结在一起。编译、链接并被打包成为一个系统。集成可以增量式地、同完成不同模块的开发平行地进行。集成只能在所有模块都完成后才能开始。
集成包括系统级别测试的开发,如果被建立的包必须要能自动安装。(可能只是解开一个tar包或从CD-ROM上拷贝一些文件)。那么这就需要以一个自动的方式去完成这个任务,或者是在冷启动系统上、或者是在热启动的系统上、或者是在容器化/模拟的环境中。
有时,在中间件(middleware)的领域,一个包只是一个创建的源代码池(source pool),在这种情况下,不存在安装工具,而且系统测试会在现成的池(as-built pool)中进行。
一旦系统被安装(若它是可安装的),自动化的系统级别测试过程就应该可以调用每个公共的命令和每个公共的输入点(public entry point)。这些调用可以带上任何一种合理的选项。若系统可以创建某种数据库,那么自动化的系统级别的测试应该创建一个数据库,然后使用外部(单独编写的)工具来验证数据库的完整性。单元测试很有可能会完成这些需求,并且所有的单元测试都应该在集成、建立和打包过程中以一定顺序运行。
区域性测试
区域性测试通常在开发人员内部开始的。这意味着由生产软件包的组织的雇员在其组织内部的计算机上进行。这将包括所有的“产品级别”的系统——桌面台式机、膝上型机及服务器。这样当你想让顾客运行他们的软件系统(或一个存在的软件系统的新版本)时,你可以告知他们“我们己经自己运行过”。在内部进行区域性测试时,开发人员应该直接为测试提供技术支持。
最终有必要将软件在外部环境运行,也就是在用户的(或符合用户期望的)机器上运行,最好是选择一个“友好”的用户进行该实验,因为在实验中往往会出现很多错误——有些甚至是实验性的错误——并且很容易被人发现。原因也许很简单,或许只是用户的习惯和使用模式与你的内部环境不同。在外部区域性测试中,软件开发工程师应该与在区域测试中发现的错误的工程师紧密配合,而且应该由高级开发人员和市场技术支持人员来决定应该在哪些文档中加以说明,哪些东西应在当前版本发布前进行修改。哪些应在下一个版本中去实现(或永远也用不着去实现)
支持
软件故障,无论是在区域性测试中发现的还是在软件被分发后发现的,都应记录在跟踪系统中。这些故障最终会分配给相应的软件工程师,在系统的定义和文档中变动,或者是在模块定义中、或者是在模块实现中变动。这些变动应包括单元和/或系统级别测试的变动,以逆向测试的形式显示出错误,以及由此显示它已被修改(并且将它保存起来,以防今后再度出现)。
正象MRD是工程和市场的结合一样,支持是工程和客户服务的结合,这种结合的结果就是bug列表、特别bug的分类,在发行软件版本中严重错误的最大数目,等等。
测试细节
涉及源代码的分析
涉及源代码的测试是以执行程序代码开始的,有时是从一个预处理器开始,有时是由一个目标代码修改程序开始,有时是使用一个编辑器或链接器的特殊模式开始。在执行过程中,对于一个源代码块中,你需要跟踪所有可能代码路径并将它们保存在记录中。着问题究竟出在哪里。
试着以下一个典型的C程序片断:
l. if(read(s,buf,sizeof buf)==-1)
2. error++;
3. else
4. error=0;
若error变量没被初始化,那么这段代码执行起来就会出错;若第二行永远执行,则程序其他部分执行的结果就不会被定义。同样的道理,在测试read(其返回值为-1)错误时,一般在测试中出错的可能性是有些低。如果你想避免对由这种bug带来技术支持而浪费精力,那么正确的方能就是在单元测试时,你进行的实验应该通过每条可能经过的代码路径,并且每条路径必须得出正确的结果。
现在,情况得到了好转。代码路径是组合的(combinatorial)。在上述例子中,变量error可能已被初始化——让我们来谈谈一个相似的代码段,其预言(“系统调用错误”)是错误的(意思是“无错误发生”)。下面的例子是一段写得很差的代码,它不会通过任何的代码检查,看看它是如何将简单的事搞得复杂起来:
l. if(connect(s,&sa,&sa_len)==-1)
2. error++;
3. else
4. error=0;
5. if(read(s,buf,sizeof buf)==-1)
6. error++;
7. else
8. error=0:
有四条代码路径可以测试:
1. 1-2-5-6行
2. 1-2-5-8行
3. 1-4-5-6行
4. 1-4-5-8行
通常不可能测试每条可能的代码路径——对于几十行代码中的每个小型函数进行测试就可能需要测试上百条路径;另一方面,仅仅确保你的单元测试(也许在后续运行中)实验每一行代码是不够的。这种范围内的分析在每个软件工程的区域性测试中不是一个工具包——这就是为什么质量保证有它的特殊性。
逆向测试
修正一个错误是不够的。有些错误属于“一目了然”,你一眼就能将它揪出来;但是还有很多隐蔽的错误,查找起来则比较困难。即使有些错误“的确”是在检查中是很容易看出来,如零在除法运算时被当作分母,但要找出什么问题的症结并排除故障,就必须看一看源代码的上下文,以找出作者(或可能是其他人)的真正意图。这种分析应该作为修正文档工作的一部分,或作为源代码注释的一部分,或者以上两者兼有。
在更加一般的情况下,错误在检查中是不易被发现的。修正工作也会在源代码的不同部分进行,而不是将程序推倒重来,或只在做得不好的地方进行修改。在这些情况下,就需要安排一个新的测试,以检查通不过测试的代码路径(或是不好的程序表达,等等),然后,根据依据新的单元测试再进行测试后续部分。在检查和改进后,新的单元测试也必须进行检查,以便以后在同样的错误产生的副作用时,质量保证人员就会有希望在用户发现之前先找到病根。
开源软件工程
一个开源软件项目可以包含上述的每一个要素的所有部分,而且公平地讲,的确有一些是这样的。BSD、BIND和商业版的Sendmail均是标准软件,工程过程的典范——但它们都没有以这种方法开始。如果要完成上面所有的步骤,那么软件工程过程将花费巨大的资源,并且在一般的情况下需要投资,而一旦涉及投资,通常就需要进行投入/产出分析。
对一个开源项目来讲,更加普遍的情况是一些相关人员对此有兴趣,并希望他们的工作成果能够尽可能被广泛使用,因此他们希望能够将他们的程序免费地尽快散发出去,并且有时对再发行不作限制。这些人可能得不到所谓的“商业级别”的软件工具(如涉及源代码的分析器,边界检查解释器,内存完整性检查器)。对他们来讲,最主要的事是从编码、打包和传道的过程中寻找乐趣,这里没有QA、MRDS这些东西(他们对此不那么感兴趣),而且你通常很难指望他们的工作具有快速的交货日期。
让我们回顾软件工程过程的每个步骤,对照地看看在没有基金资助的开源软件项目中(开源软件项目通常是靠具有热情的人完成的),在典型的情形下会发生了什么。
市场需求
开源项目的开发成员希望建立他们需要或想要的工具。有时,这与他们的日常工作有关,经常是他们的主要工作是系统管理,而不是软件工程人员。如果在几次循环后,明确了一个软件系统应该具有的哪些关键功能,那么这个开源项目就诞生了,开发入员会通过Internet包的形式散发,其他用户会逐渐开始询问其特性,或者坐下来实现它们,并将发回给提出任务的人。
开源的MRD通常是邮件列表中或者在组中提出的,用户和开发者之间来来回回直接的善意取笑逗乐,舆论是任何开发人员都记得或同意的。没有达成一致意见常常导致出现“代码分支”的,其他的一些开发者就会开始发布他们自己的版本,在开源的环境中,等同于MRD的资源是非常丰富的,但它有其极限——矛盾的解决方案有时是不可能的(或没有人去尝试)。
系统级别的设计
通常对于没有基金的开源软件来讲,是没有系统级别的设计而言的。情况可能是系统设计本身可能是隐形的,或者直接由宙斯的脑子里蹦出的;或者是时间的产物(就象软件本身一样)。通常当某个开源软件系统推出第二版或者第三版时,会有一些系统设计,即使它没有被书写成文。
正是在这里,与需要循规蹈矩的软件工程的一般规则相比,开源软件因其一些怪异性才赢得了信誉。正是因为有真正好的程序员(或者真正友好的用户),才补偿了缺乏正规的MRD或者正规的QA可能出现的问题,但如果没有系统设计(哪怕只是存在于某人的头脑里),项目的质量将会受到自身的限制。
详细设计
因为没有资助和需要取得乐趣的另一个不足的方面会体现在详细设计中。某些人的确为DDD工作其乐无穷,但是这些人通常是因为在他们的日常工作中能够编写DDD来采取得乐趣。详细设计作为执行的副作用面而终止。“我知道我需要一个解析器,所以我要写一个。”在头文件或manpages中以外部符号来为API写文档是可以选择的,并且在如果API不被该项目公布出去或不被外部人员使用时是不会发生编写文档的事情的。
这是一个耻辱,因为大量优秀的和其他可重复使用的代码在这种方式下被隐藏起来。甚至在项目中较少作用且没有被重用的模块,以及所带有的API不是要交付的功能中的部分时,往往需要有Manpages来解释这些代码要做什么,以及怎样调用它们。这些内容对于其他想要增强这些代码的人来讲是非常有用的,但是往往缺乏说明,所以其他人不得不去阅读和理解这些代码的含义。
实现
这是一个有趣的部分。实现设计是程序员们最喜爱的部分;这可以使得他们在应该在睡觉的时间里进行hack。有机会写出好代码是几乎所有开源软件开发者们的主要动力,同时这也极大地促进了开源运动的发展。如果一个人专注于软件工程的某个方面而排除其他方面的干扰,那么就会有表述上的巨大自由。
开源项目能让大多数的程序员尝试新的编程风格,这可能是以缩进样式书写程序代码,或者变量命名,以及“试图去节省内存”或“试图去节省CPU循环”,或是其他一些你想要的一些风格——程序员尽可能地首次尝试某种新的风格,并使程序“能够运行”。
一个没有资金支持的开源项目可以努力尽可能多地保持严密性和连续性——如果代码具有某种功能,那么用户会运行代码;大多数人不会在意软件开发过程中,开发人员是否多次更改过风格,但是开发人员通常会留意这些变化,或者他们在某段时间内留意这些变化。在这种情况下,Larry Wall过去曾经评论说:编程就像是在完成一种艺术表达。这句话真是精辟之至。
没有资金资助的开源软件的实现,与正规的软件工程的一个主要不同之处是,对代码的检查是不规率的。通常在代码被发布之前,没有指导者或者同事来查看代码,通常也没有单元测试、正规的测试或其他测试活动。
集成
开源项目的集成通常包含编写一些manpages,确认在开发人员使用的所有平台上都能够运行,将Makefile中把程序实现阶段所含的一些多余成分清除掉,编写一个自述文件,创建tar包,将这个tar包放到匿名的FTP站点上,并且向一些邮件列表和组发布通知,以便让感兴趣的用户来使用。
值得注意的是在1998年,comp.sources.unix组由Rob Braun负责重新运转越来,该组是适于发布新的或更新开源软件信息的好地方。同时它也可以被看作是一个程序库/文档库。
这样也好,没有系统级别的测试。但是接下来,开源软件项目通常也没有系统级别的测试方案和单元测试方案。实际上,开源软件在全面测试上的努力非常少(当然也有例外,如Perl和PostgreSQL)。这种预发布前的缺乏测试并不能算是一个弱点,理由可以从下文获得。
区域性测试
没有资助的开源软件可以得到业界最好的系统级别测试,只是无法与NASA对太空机器人的测试相比。原因很简单,因为在不需要付钱的时候,用户会更加友好,并且当用户可以读、改他们正在运行的程序的源代码的时候,这些用户(通常这些用户也是开发人员)会对软件的开发带来更大的帮助。
区域性测试的精华在于它缺乏严密性(rigor)。软件工程期待从区域性测试者那里得到的东西,在系统性计和建立时天生就是不可预测的——换句话来讲,这就是真实用户的实际经验所在。没有资金支持的开源项目在这一方面的优势是无与伦比的。
对于开源项目来讲,一个附加的优点是,通过阅读程序源代码,成千上万的程序员可以来共同查找bug,即“同侪检查”(peer review),而不仅仅是检查执行程序包的执行文件。某些用户会找到一些安全性的瑕疵,这些发现中的某一部分不会被报告(在某些骇客之间),但是这种危险不会在这里发生。因为由难以数计的人们在对源代码进行全面的检查。源代码中的问题很难逃过所有人的眼睛。
支持
“噢,对不起!
”这通常是在用户发现bug时开发人员说的话,或者如果他们的用户在报告bug的同时也提供了补丁程序,开发人员会说“噢,真对不起,谢谢你!
”。“嘿,它可以运行了”是通常在开源软件开发人员在筛掉(triage)bug后说的话。可能你会觉得这听起来有些混乱,但实际上就是这样。由于缺乏支持,有些用户对没有基金支持的开源软件望而生畏,但同时这也为咨询顾问和软件分发商们提供了一个机会,来签定技术支持合同以增强开源软件商业版本的发行量。
有些用户强烈要求将开源软件间UNIX的基本部分预先打包在一起分发,当Unix商家社团第一次接收到用户的这种要求时,他们的第一反应多少让人感到冷冰冰:“好的,我们可以将它们集成到系统中,但是我们不能对这些软件提供支持。”一些成功的公司如Cygnus已经提出了要重新审视这一局面,但是文化的冲突与隔阂非常之深。传统的软件开发机构,包括Unix供应商们,如果他们没有改变思路考虑这些用户作出的贡献,那么他们不可能对技术支持事先进行规划并提供资金预算。
在某些场合下,答案是从软件的内部来解决问题,通过一般的QA运行处理,包括单元和系统测试,涉及源代码的分析等检查。这可以从逆工程方向涉及到MRD和DDD,为QA提供某种背景(即功能测试要涉及的东西)。另一种答案即是重写支持协议中的条款,将“保证结果”改为“尽最大努力”。最终软件支持的市场将会由那些能够为所有未知的用户提供技术支持的人所占有,因为他们中大多数人非常好,并且可以写出很优秀的程序。在某些用户真正想实现某种级别的功能时,在大多数场合下,开源文化将会更为有效。不信的话,你可以亲自比较一下Linux和Windows。
结论
工程学(engineering)是一个古老的领域,而且无论是编制软件、生产硬件或者是修路架桥,工程处理过程的基本组成部分大体是相同的:
明确需求,以及需求本身的要求。
设计一个方案来满足要求。
将设立计模块化;计划如何实现设计。
建立;测试;提交;支持。
某些工程领域将重点放在一些阶段上。例如,修路架桥的人通常不会把思路放在一份MRD上,或者实现过程中,或者支持阶段上,而且会将更多的注意力放在SDD、DDD和QA。
在将“程序员”(programmer)转变为“软件工程师”(software engineer)的最初阶段,他们应该立刻意识到工程作为一个即将涉足的领域,它需要从根本上具有不同的思维(Mindset)——并且有大量的工作要做。在开源软件的开发者们在意识到编程和软件工程之间的差别之前,他们已经成功地以原有方式工作了许多年,他们会很快适应新的要求——原因很简单,即众多开源软件项目己经长时间里经历了由于缺乏工程精确性而造成的痛苦。
本文仅简单回顾了软件工程,希望能够由此为开源软件的开发者们提供一些动力和有关软件工程前后发展的历史,从而为进入软件工程的领域提供一些思路,因为将来总是以前和现在的诸多情况的综合体。软件工程不仅仅是只为了计算滑尺(slide rule)和口袋型防护装置(pocket protector set)服务的——实际上,它拥有大量已被证实是可靠的技术,可建立众多高质量的系统,它不仅对“一个聪明的程序员”有效、而且对创建所有高质量开源工程系统都适用。
作者简介
Paul Vixie
Paul Vixie是Vixie Enterprises的领导人。他同时也是Internet Software Consortium的创建人和主席,该俱乐部创建了BIND、inn和dhcp服务器。在DNS中,Paul设计的BIND是使用最为广泛的软件包,而他是BIND的总设计师。Inn是Internet服务器软件包,dhcp允许动态配置网络信息。
Paul是Vixie cron的开发者,该软件被作为Linux的缺省形式下的cron看守程序使用,并且被世界大多数其他Linux软件包所使用。这也许意味着,Paul可能会对每天半夜一点在你计算机上发出的奇怪声音负责。
Paul是Sendmail: Theory and Practice一书的作者。Paul的公司也为Commercial Internet Exchange管理网络,并且同MAPS(Mail Abuse Protection System)一道领导同垃圾邮件的战斗,该系统生成了一个黑洞列表(将垃圾邮件制造者们的邮件扔到一个无限容量的桶中),并且主动传输安全机制。
i4CN(工业4.0中国-简称),是中国最系统化、最全面的工业4.0、工业互联网、智能制造、无人工厂领域的第三方咨询公司。公司整合华为、博世、腾讯、美的等专家,首家提供工业4.0整合方案,包括i4技术项目、i4四大管理体系、十大思想变革的三层金字塔式咨询架构;能够指导企业实施专业化的工业4.0变革和无人工厂规划建设与运营管理。助力国家实现中国制造2025的宏伟蓝图。
梁卓业 i4CN首席咨询顾问中国工业4.0、智能制造、无人工厂、工业互联网专家,华为ISC、IPD体系专家华为ISC+项目组成员,智能制造标杆车间项目经理工业4.0十大思想变革、无人工厂建设体系首创人中山大学麻省理工学院双MBA,广东工业大学机电学院本科欢迎需要导入华为ISC、IPD体系,实施工业4.0无人工厂的企业与i4CN合作。
(请搜索i4CN梁卓业老师相关课程视频并进一步了解)