浅谈软件的特性及其意义

 人们往往不愿意浪费时间去认真思考软件的本质特性,其后果常常是使用了错误的方法去开发软件。

  软件是人类有史以来创造的一种非常特别的制品,它具备与传统制品完全不同的特性。而软件的这些特性既来自信息技术本身,也来自其所要解决的问题域。正是它的这种特殊性,使得人类至今仍然在苦苦寻觅解决软件开发问题的终极手段——所谓“一枪毙命的银弹”。

  从1991年起一直从事IT行业,长期担任软件开发的高级技术和管理职务,拥有非常丰富的工作经验(其中有2年硬件开发的特别经历);曾经成功主持过多个大型软件项目及软件产品的开发;是国内较早引入UML模型驱动开发方法的倡导人之一,和最早的UP统一软件过程推动人之一;目前是国内培训与咨询界最受欢迎的资深软件架构师之一,和屈指可数的能够领导开展软件教练项目的顶级咨询师之一。

  曾经给东软、华为、中兴通讯、雅虎中国(3721)、西门子、海南航空集团等知名企业做过教练项目或咨询培训。

  软件的精确性与模糊性

  大部分人造物,比如象建筑,都可以允许一定的误差存在。我们在修建公路时,并不苛求最终的路面位置与设计图中的坐标值丝毫不差;在建造楼房时,也不要求施工队浇注的钢筋混泥土框架与设计的承重指标精确相符。一方面,这是因为人类还没有能力以合理的成本在建筑上实现极高的精度;另一方面,人类可以通过留有余量的手段来解决误差问题。例如建筑师在设计楼房时,会为承重指标留一定的余量,即使施工队浇注的框架结构强度比设计指标低一点,也不会因为这种不精确而导致其坍塌。(留余量、多付出一定的代价以规避风险的做法,实际上是传统工程的一种通行原则,称为Overengineering过度工程或过度设计;极端的例子是运载火箭上的控制设备至少备有三套,以防控制指令出错)

  但是软件却不像上述其它人工制品,软件的最终交付形态是二进制的可执行码,执行码是不能容忍误差的。想象一下,我们复制一段可执行文件到主机上去运行,如果文件中的某个字节出现了错误,这时我们恐怕只能向上帝祷告,祈求系统不要突然挡机了。据说欧洲亚利安娜火箭一次发射失败的主要原因,就是其飞行控制软件中的一行代码有误所造成的。实际上,程序代码中的任何误差,都可能导致软件的整体失效;人们也无法通过简单地留余量的(过度工程)方式来解决这种问题。

  其背后的原理是:建筑设计的客体内容(结构、材料、工艺等)本身并不是精确的东西(取值是连续的);而软件开发的客体,是计算机的执行序列码,它本身是绝对精确的东西(取值是离散的,最终会转化为机器中代表0/1的电平信号),不允许误差的存在(当然,如果未来哪一天,神经元计算体系取代目前的冯·诺依曼体系,局面可能就完全不同了)。

  软件的这种精确性,迫使人们在开发时,必须投入大量的精力来确保代码的正确性。而人类的思维特性正好与之相反,是模糊和充满误差的。因此,程序员很难在第一次就写出正确的代码;他总是要不断的测试这些代码,然后调试、跟踪,进行除错,直到获得正确的结果。

  这样,软件的测试与验证在开发活动中就占据了极其重要的地位。另外,其它行业中很少会出现软件开发中的一种特有现象——程序员大部分时间中不是在编写新的代码,而是在排除已有代码中的各种Bug;更要命的是,程序员自己也没有把握,在其承诺的时间内,将主要的缺陷都排除掉。

  软件的精确性还体现在软件的规格定义上,如果软件的需求规格没有定义准确,那么据此所开发出来的交付也不大可能符合用户的要求。

  理论上已经证明,程序的百分之百正确性是不可能实现的。因此软件同样无可奈何地需要容忍一定误差的存在,但这种误差主要体现在软件的某些质量范围上,而软件的功能本身在逻辑上仍然是排斥误差的。

  软件中的可以接受的误差或不精确主要体现在:

  ◆ 交付的可靠性误差——用户对交付中缺陷的接纳程度(用户能够接受一些非关键功能的失效)。开发组总是尽量减少交付中的缺陷数量,但实现零缺陷在理论上是做不到的;因此,只要将交付的缺陷率控制在一定范围之内,能够为用户所容忍就行了。

  ◆ 软件的健壮性误差——软件对规格定义之外的异常状况的适应能力(用户总是可能执行一些非法的操作等)。开发组应当在规格定义中就将异常处理囊括进去,并在设计时考虑更多的异常处理场景;这样用户的非法操作不至于引起系统的崩溃。

  ◆ 系统的性能误差——软件性能处于用户能够接受的范围之内。开发组尽力设计性能最优的方案,比用户的接受标准更高,为今后系统的演进留下更大的余量(例如用户数量随业务的发展而呈几何指数增长)。

  ◆ 范围类规格误差——软件中涉及到一些规格定义,这些规格本身是一种范围定义;开发组在不确定的时候,可以选择定义更大的范围值来规避风险。例如,为了避免数组的溢出,开发组可以尽量定义更长的数组结构;某个函数的某个输入参数,在运行时刻可能会是很大的整数,开发组将选择长整型作为参数的类型规格。

  ◆ 其它误差——例如,软件的易用性本身就是一个相对模糊的概念,开发组有很大的自由空间来满足它。

软件的规模性与复杂性

  与软件类似的是数字电路,它同样不能容忍误差的存在;但幸运的是,数字电路的复杂程度不如软件那么高,这避免了追求精确与复杂性交织在一起所带来的巨大挑战。

  从规模上看,先进的CPU可能集成了几千万个晶体管;但是这些晶体管元素的结构存在惊人的相似性。例如,一个主流的CPU可能具有4Mbyte大小的2级缓存,但其存储单元的基本结构都是一样的,只是重复了四百万次而已。这样,我们在设计这个CPU时,2级缓存部分实际上只需要设计一个存储单元,其余的都是机械的复制而已。因此,数字电路的复杂度,并不与其晶体管的数量成正比。

  参考图1中Intel双核处理器芯片的晶圆照片,我们可以看到其结构整齐划一,大片的细节都非常相象。

  相对应的,在开发一个百万行源代码级的软件时,却享受不到这种重复的乐趣。实际上,软件中如果存在大量拷贝、粘贴式的重复的话,反而会给其维护带来灾难性的后果。软件与数字电路不同,功能完全一样的元素,在代码上都应当力求只出现一次。面向过程的语言,支持人们将重复的功能抽取成单独的子函数,并被重复调用;而面向对象的语言,则进一步提供了实例化机制的支持,鼓励人们将重复的内容定义一次(类),然后在运行时刻大量地实例化它们(对象)。因此,一个结构良好(低冗余度)的软件,其源代码行数基本上能够反映它的规模和复杂度。如果要寻找一种指标,来衡量人类对复杂事物的最高驾驭能力,那么,统计现有最复杂、最具挑战性的软件的所有源代码行数,也许是最有说服力的做法。

  在第一节,我们已经阐明软件用于处理业务(或者其它人造系统,比如国防武器系统)领域中的各类信息;也就是说,软件的内容要反映其所面对问题域中的事物。正是因为软件所承载的问题领域极其广泛和复杂,使得软件空前地复杂和规模庞大。例如微软的Vista操作系统,其源代码行数已进入千万数量级;可以理解的是,为了兼容成千上万种不同厂家所生产的硬件设备,每种设备都开发对应的驱动,那么,光是这些驱动软件,恐怕就占据了千万行源代码中的相当比例。

  总之,软件的复杂性来源于其面对问题域的复杂性,和为了支持问题的解决,设计软件本身所带来的复杂性。

  问题域本身的根本复杂多样性,通过工具或普通方法是解决不了的;而因为象使用汇编语言来开发业务软件之类做法,所带来的附加复杂性,则完全有可能使用更先进的工具来彻底解决掉(“软件是对现实世界的一种映像”段落图示中,UML等建模语言在描述业务时,就远比汇编语言来的高效)。

  软件元素之间的关联复杂性

  人类对于复杂的事物,通常采用分而治之的途径来解决它。以往行业,比如汽车制造,可以将整车划分为若干个零件,先将这些零件制造好,然后再组装起来。汽车零件相对独立,相互之间的依赖比较松散;只要各个零部件的质量过关,最后组装起来的整车也基本没问题。软件的内部组成部件,通过良好的设计,同样可以做到一定的相互对立性;然而无论如何,集成后的整体软件行为,与单个部件的行为之间还是存在着巨大的差异。所有软件单元的质量通过了验证,并不等于集成后的交付也能够通过验证。

  造成上述这种现象的根源,在于软件内部部件之间的关联比其它产品要复杂很多。软件所支持的问题域本身,其元素之间的关联与依赖关系就比较复杂;而软件本身所使用的各项技术,通常相互交织一团,并与问题域的内容整合在一起,造成部件之间的关系复杂度增高一个数量级。

  传统项目,例如公路项目,投入的人力与工期能够互换;即增加人力,可以成比例地缩短项目的工期。然而,软件项目,由于软件元素之间的关联过于复杂,使得项目成员无论如何组织和分配任务,其相互之间的依赖性都很强。依赖的存在,造成项目成员难以并行工作,在已有成员都可能被迫停下工作来等待时,增加的人力无法对项目产生贡献。元素间众多复杂的关联关系,使得承担不同元素的项目成员之间,需要就这些关联进行对应的沟通,这种沟通量将随参与人数成几何指数增长;因此,增加人力后,即使能分担一定工作量,但其同时又增添了沟通上的不必要开销。总之,软件项目中,人月不能互换。(参见《人月神话》)

  软件的不一致性、多样性

  普通行业的问题域只有一个,例如建筑设计面对的问题不外乎就是如何设计合适的受力结构、使用合适的材料来构建一个满足各类建筑规则(力学等)的建筑体;另外,建筑行业经过多年的发展,已经非常成熟,建筑师能够利用大量的现成经验,使得其设计(解)空间比较窄小。

  软件则要同时面对两个问题域。除了业务运作的问题域本身,还存在如何设计软件来支持解决业务问题的工作。在“软件是对现实世界的一种映像”段落中,我们看到问题域距离计算机域非常遥远;要将目标问题域最终转化为软件解决(机器码),中间还有大量工作要做。软件的解决方案,从普遍意义上看,不像建筑设计那样,存在明显一致的模式来套用,也没有一致的规律可循。企业级业务应用软件所采用的架构样式,与CAD桌面软件的就很不一样;实时软件常用的设计模式,与网站应用软件就毫无瓜葛。

  企业业务流程的一个重要特点,就是其多样性和创新性。因为市场的竞争压力,企业业务流程不能同质化,而且变化非常频繁。业务问题域的多样性,带来了软件的多样性。

  另外,普通行业的问题域,关注的是最终目标产品,例如建筑设计关注目标建筑本身的规律和约束。而软件的问题域,关注目标却要涵盖产品生产的整个过程。例如,我们如果开发一个建筑的设计辅助软件,我们要研究建筑体本身的规律和约束,但同时,我们更关注如何进行建筑设计的过程和方法;因此软件问题域的范围极大地被扩展了。

软件的不可视性与主观性

  从严格的意义上来讲,软件没有对应的物理存在形式。一张软件交付光碟,光碟(物质)本身只是一个载体或媒介,而软件则附着于那些0/1比特所构成的表示之上。虽然我们可以使用工具来间接观察软件的内容,但是在其被计算机执行之前,我们还是不能直接领会这些0/1比特所代表的真实意义。因此,与其它物品不同,软件在物理上是不可视的,而且也不能通过简单的工具来观察它。

  人们完整和准确地感知软件的唯一途径,是在主机系统上运行软件;通过给系统以输入,然后观察系统的响应输出,来观察系统当前所运行软件的行为。如果软件使用了GUI图形界面的话,人们将对软件产生形象上的感知。另外,人们还可以感知软件执行所表现出的、在秒级以上范围的性能属性;对于毫秒级的性能,人们不能直接感知到,只能通过数据收集,以生成分析报告的方式来认识它们(性能测试比功能测试困难的原因就在于此)。

  对于业务软件而言,其支撑的业务过程本身也存在不可视的问题。业务活动是动态的,我们不能以静态的方式来观察它;同时,业务过程又是长期的,我们无法以业务活动的某个片断来揭示整个业务的形态。这样,业务软件因为业务本身的不可视性,变得更为难以观察与感知。

  软件的不可视性还表现在软件的开发过程上。传统工程,例如修建一条公路,设计师只要拿出设计图纸,那么整个工程的工作量、成本就可以较为准确地估算出来。然而,在软件交付完成之前,人们却永远也不能真正精确地估计开发的工作量。

  软件的不可视,给软件开发活动带来了更多的主观性色彩。

  软件被正确验证,取决于对软件交付行为的准确观察。而不能被执行的软件半成品,是不可视的。传统瀑布生命周期开发模型的失效,很多时候源自需求和设计没有被真正验证;瀑布模型下,可运行的交付在项目后期才能得到;在这之前,人们只能通过评审等手段来验证设计;而评审不能直接观察软件的行为,自然也就不能客观地对软件设计质量进行评判。

  软件开发工作量、成本难以准确估算,使得软件项目计划总是充满了主观臆断。而项目的进展是否健康,项目经理也常常只能依赖其经验来做主观的判断。

  测试是软件验证活动中唯一客观、也是最为可靠的途径。业务过程的长期动态特性,使得对应软件的测试工作难度成倍增大。

  软件的易变性(可塑性)与不确定性

  软件所面对的问题域本身经常就是含混与不确定的,这迫使软件需求要随着领域的变化而进行调整。另外,软件对同样的业务,可以有不同的需求方案(软件复杂一些,使得业务的自动化程度高一些;而软件简单一点,同样也能改进业务的效率),这造成了软件需求的不确定。软件设计中,针对同一需求,可能存在多种实现方案;这也增加了软件开发的不确定性。

  另一方面,软件本身容易被更改,可塑性malleable较强;这使得人们常常忽视变更背后的成本和潜在风险,更倾向于去决定变更软件。例如,有的客户往往不情愿在一开始就与开发者将需求的细节讨论清楚,并确定下来;而是指望在发现问题后,开发者有神通能迅速地修改好。而程序员往往也不喜欢花费太多的时间先去完善设计,而是直接投入编码,等到发现一大堆Bug后,再去一一修改。

  软件的不确定,迫使软件不得不变化;而软件容易被更改,又加重了人们轻易地决定变更软件的冲动。

  软件易变性也有其有利的一面。人类思维的局限性,使得人们不可能一开始就做出一个完美的设计。软件的可塑性,则支持人们逐步精化、改进软件,乃至增量式地开发软件。例如,一条设计为四车道的公路已经构筑了一半,突然更改设计变为六车道,经过工人们的努力,总算变更成功;然而,我们总是会很容易地看出这条公路被改造过的痕迹(感官上将很别扭)。相对的,软件发生重大变更时,只要变更不超出可控范围,经过努力是有可能不在最终交付中留下任何被更改痕迹的(我们阅读一段最终交付代码,如果没有变更记录的话,通常很难看出它们到底经历了多少次的修改)。

  有些预研性的软件,一开始很难想象其整体架构的模样;于是可以先从某个局部开始进行探索式开发,逐次开发一些代码来解决不同的问题,最后以滚雪球的方式整合出最终交付。这实质上是一种自底往向上的开发模式。软件的可塑性使得自底往向上的开发顺序在一定范围内有效。例如,汽车的生产过程,是先制造好所有的零部件,然后装配成整车的;然而,那些零件是根据事先设计好的规格来制造的,否则它们无法被整装在一起;汽车的生产,实质上是先自顶向下进行设计,然后自底往向上进行构造。而软件的自底往向上,很多时候是脱离整体设计而自然发生的;软件的可塑性,使得人们在集成时,可以重构这些原本独自开发的部件(对于不能更改的部件,还可以使用适配机制),最终将它们集成为一个完整的交付。

  实际上,任何主要由人类思维来加工的制品,都具有一定的可塑性。回顾上文中汽车的例子,虽然到了物理生产阶段,汽车很难再被更改;但是在设计阶段,设计师却还有一定的空间来修改他的设计方案。这是因为物理形态的汽车变更成本高昂,而承载人类思维成果的图纸或模型,变更则比较容易。软件实质上是反映人类解决领域问题成果的一种载体,其物理形态主要为源代码,变更的成本较低。软件的复制式生产

  与其它人造制品的生产不同,人们能够通过几乎零成本的复制方式来批量生产软件。这是因为软件本身也是一种信息,而信息是能够几乎零成本地被复制的。

  正是这种零成本的复制生产方式,凸现了软件的可塑性。对软件代码的任何修改,只要使用工具重新自动编译一遍,就能轻易得到新的源本,之后再大量的复制,新的一批软件产品便这样被生产出来了。

  这也决定了软件的质量,在生产阶段是没有太多实际意义的,因为软件的复制过程,可以非常容易地保证副本与源本不出现任何偏差。我们所关注的软件质量,应当是它被开发出来的那个原始样本的质量。

  链 接

  消除冗余的终极手段

  如何消除软件中的冗余,实际上是软件架构设计中的一个核心问题;有多种途径来去冗余,例如继承、元数据驱动技术等,我们将在后续章节专门做深入的探讨。

  假设未来某一天,数字电路的运作原理发生革命性突破,我们或许可以模仿软件的机制,先定义好单个存储单元的结构,然后由cpu自己在物理上实例化出四百万个一摸一样的存储单元来。这样,数字电路的物理实现上便同样不再存在简单的复制类冗余。

  

  图1 Intel最新的双核处理器芯片晶圆照片

  链 接

  修复bug的痛苦之旅

  由于人类的思维局限性,造成任何人造制品中都可能出现错误或瑕疵。软件对精确性的高要求,以及其本身的复杂与不可视性,使得软件开发过程中引入缺陷的概率比其它行业大了一个数量级;同时,其修复缺陷的难度也高得多。

  笔者在以往编码生涯中,感觉最痛苦的事情,莫过于去排除程序中的bug。修改bug本身其实并不难,真正的困难在于如何从成千上万行代码中找到它的病根。软件bug之所以被发现,是因为可以观察到其症状的表现;但是,引起症状的根源却隐藏在软件内部的任何一行或多行代码中,并不能直接看到它们。定位bug的根源,很多时候是需要极高的耐心与毅力的。

  程序员在缺乏排除类似bug的经验时,只能先努力观测症状,再去猜想bug可能的根源,然后尝试修改对应的代码,之后运行之,以验证bug是否被消除。如果运气足够好话,bug的症状可能就不再出现;这意味着,程序员也许终于不必再重复上述观测、猜想、尝试和验证的过程了。

  然而,猜想最终被证实大多是要经历无数次反复的。另外,很有可能,程序员甚至永远也猜不透那个bug的谜底。

  改善缺陷修复效率的主要途径,一是单元测试,这是因为每个单元的代码可以控制在几百行、甚至几十行之内,一旦在单元测试中观察到bug的症状,那么,这个bug的根源只是局限在这几百行代码中而已;另外就是增量式的开发,每次只是增加几百行代码,然后就进行全面的系统测试(这便是所谓的持续验证测试),一旦发现bug,那么其根源隐藏在新增少量代码中的概率非常大(增量式开发加上持续验证,能够解决单元测试所不能解决的系统级bug定位问题)。

  链 接

  摩尔定律的幻觉

  IT界中有一个著名的摩尔定律,即电脑芯片中晶体管的数量每18个月将翻一番。这常常给人一种幻觉,以为数字电路的复杂度在急剧地膨胀,因而人类处理复杂事物的潜力也几乎没有极限。

  上文所述的2级缓存例子,说明了一个事实—摩尔定律的存在,并不能证明人类的能力,真的象晶体管数量每18个月就翻番一样,在同步地提高。

相反,微软Vista操作系统的不断跳票,若干大规模软件的最终失败,以至于所谓“软件危机”概念的提出,却实实在在地严重打击了人类在解决复杂问题时的自信心。

 

——于2007年

 

来源:曹牧

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2011年6月10日
下一篇 2011年6月10日

相关推荐