开发体验

第15章 软件工程与应用测试

“你好猫咪”“打地鼠”以及前几章中讲过的其他应用,都是小型的软件项目,并不需要大量软件工程{![工程的概念是从传统行业中借用而来的,意为设计并建造。对于某些极其复杂的建造过程,前期需要经过构思、规划、分析、设计等环节,之后才能转入实施阶段,这些过程都可以归结到工程的概念中。——译者注]}。只要做过一个相对复杂的项目,你就会理解,只要在功能上稍稍增加一点复杂度,软件工程的复杂程度就会急剧增加,两者之间绝对不是线性的关系。本章将带你快速地了解如何创建相对复杂的软件,其中包括需求分析、功能设计、规格说明、用户测试及系统测试等。一般来说,这不仅需要具有一定的编程能力,还需要掌握一些工程技术与技巧。对大多数人来说,经历挫折是在所难免的,然而这是成长乃至成熟的必经之路。为此,你要学习一些软件工程的原则及调试技巧。如果你认可这一点,或者你是为数不多的希望通过学习来扫除成长障碍的人,那么本章就是为你准备的。

软件工程原则

以下是本章涵盖的一些基本原则:

  • 未来的软件使用者应该尽早并尽可能多地参与到软件的设计及开发过程中来;
  • 建立一个初始的、简单的原型,并逐步完善;
  • 编码与测试同步进行,不要一次编写和测试太多的代码;
  • 在正式开始编码之前,先设计应用的逻辑;
  • 对功能做纵向切割,对技术或实施的复杂度做横向分解,并各个击破;
  • 对代码块进行注释,以便其他人(和你自己)能理解这些程序;
  • 学会用纸笔来跟踪记录块的执行过程,以便于理解它们的工作机制。

如果能够遵循上述原则,你就可以节省时间,减少挫折,并最终制作出优秀的软件。但是,要做到凡事都遵循原则并非易事!有些原则看似违背常理。如果不考虑这些原则,我们的开发过程可能是这样的:首先有了一个想法,并假设自己了解用户的需求,然后开始动手摞代码,直到完成了想象中的任务。现在,让我们回到软件工程的第一个原则,在正式开始动手之前,看看如何了解用户的需求。

解决真实的问题

电影《梦幻成真》中的男主角Ray听到了一个声音向他低语:“如果你建好了,他们就会来。”Ray听从了这个声音,在爱荷华州的农场中间建了一个棒球场。果然,1919赛季的芝加哥白袜队(White Sox)以及成千上万的球迷们齐聚在这里。

不过你现在必须明白,那种带有暗示性的“低语”绝对不可以用于指导软件开发。事实上,你要做的正好相反。在软件开发的历史中,有各类“没问题”的伟大方案。只有解决真实问题的应用,才是受欢迎的、成功的应用,也才是能够盈利的应用。要了解哪些是真实的问题,必须与真正的使用者对话。这就是所谓的以用户为中心的设计理念,它将有助于你开发出更好的应用。

如果有机会与程序员们聊天,你可以问问他们,在他们所开发的软件中,有多少被真正地交付到了最终用户的手中。结果会让你感到惊讶:即使是对于程序员中的高手来说,这个比例也非常低!许多软件项目由于陷入问题的泥沼,而最终也没能见到天日。

以用户为中心的设计理念意味着尽早并尽可能多地替未来的使用者着想,并与他们交流,这种思考与交流甚至应该在尚未确定目标之前就开始。大多数成功的软件都是针对某个具体的人,试图解决他的特定问题,也只有这样,最终才能成就一个伟大的作品。

向用户展示原型产品

如果让最终用户阅读软件功能的说明文档并给出反馈,他们多半不会给出任何有效的反馈。真正有效的方法是,让他们体验未来软件的交互模式,即软件的原型。原型是一个不完整的、未经完善的软件版本。创建原型的目的在于充分体现软件所具有的核心价值,所以不必注重细节、完整性或漂亮的图形用户界面。拿出原型让未来的使用者看,然后安静地倾听他们的反馈。

渐进式开发

当首次开发一款略显复杂的应用时,你会不假思索地将全部组件和代码块一股脑地添加到应用中,然后下载到手机上看看它是否好用。以一个问答类的应用为例。在缺乏指导的情况下,多数初学者会一次性添加所有的代码来实现所有的功能:初始化长长的问题列表及答案列表、翻阅所有问题、检查用户的答案,以及程序逻辑的每一个具体细节,所有的块未经测试就全部罗列在应用中。这种开发方式在软件工程中被称为爆炸式开发

几乎所有的初学者都会采用这种方式。在我(本书作者Wolber教授)的课堂上,当学生忙于创建应用时,我经常会问他一个问题:

“进展如何?”
“我想我做完了。”学生回答说。
“好极了,能让我看看吗?”
“哦,还不行,我没有带手机。”
“那么你还从来没有运行过这个程序,对吗?”我问。
“嗯。”

我透过他的肩膀看到了几十个色彩缤纷的代码块,但他居然连一个功能都没有测试过。你可以这样做,但问题是,当你想一次性地测试所有代码时,一旦测试失败,你根本无法确定究竟是哪部分的代码出了问题。要知道,出错是必然的,而且会一团糟。

对于我的学生,以及那些有志做一名程序员的人,我能给出的最好的建议是:

写一写,测一测,周而复始。

每次只写少量代码,并及时测试。出错在所难免,但没关系,局部的问题可以轻而易举地解决。这个过程会逐渐演变成一种习惯,这样,在不久的将来,你会收获惊人的成果。

有大量关于渐进式软件开发的书籍和论文。如果你对软件开发过程(或类似的问题)感兴趣,可以参考敏捷开发方法{![Beck、Kent等人于2001年成立敏捷联盟,同时发布“敏捷软件开发宣言”,并于2014年6月5日做最后一次修订。]}方面的图书。

先设计,后编码

编程要分两步走:(1)理解应用的逻辑,(2)将这些逻辑翻译成某种形式的编程语言。在开始翻译之前,要在逻辑上花一些工夫:首先要明确应用中将会发生哪些事件,无论是用户引发的,还是应用内部的;其次在正式开始将逻辑翻译成代码之前,要明确每个事件处理程序中的逻辑。

有许多专门论述各种程序设计方法的书籍。有些人喜欢用图来表达设计思想,如流程图或结构图,有些人则更愿意将设计或草图写在纸上,更有人认为所有的“设计”最终应该体现为代码的注释,而不是一份与代码分离的文档。对于初学者来说,关键是要理解所有的程序在本质上都是一系列逻辑,而这些逻辑与具体的编程语言无关。当然,梳理应用的逻辑,将逻辑翻译为编程语言,这两件事有时难免会合二为一,而无论你使用哪种编程语言,直观的或是抽象的,这种倾向都是难以抗拒的。因此,在整个逻辑设计阶段,应该远离电脑,想清楚应用最终要实现哪些功能,然后将自己的思路完整地记录下来形成文档,并在后续的编码过程中,建立代码与文档之间的关联,这样,其他人也可以从中受益。

对代码进行注释

你已经学完了本书中的前半部分——AI2教程,应该见过图15-1中的黄色方框,这就是“注释”,它附属于某个代码块。在App Inventor中,任何的块都可以添加注释,方法是在块上单击鼠标右键,并在快捷菜单中选择“添加注释”。注释用于解释代码的作用,丝毫不影响程序的运行。

图15-1 为条件判断块添加注释,用简洁的语言描述块的作用

那么为什么要做注释呢?想想看,如果你的应用取得了成功,那么它的生命周期将会很长,这意味着还有后续改进和升级的可能。编程就是这样,虽然是自己编写的代码,但哪怕只是搁下一周的时间,你都可能忘记当时的想法,想不起来为什么要使用这些块。因此,即使没有其他人会看到你的代码块,你也应该添加这些注释。

假如你的应用非常成功,毫无疑问它的源码会被很多人下载。人们想了解它,并按照自己的需要修改它,或者扩展它的功能,等等。在开源的世界里,很多项目都是以现有项目为基础,做进一步的修改和完善。如果你研究过那些没有注释的项目代码,你就会明白为什么注释是必需的。

为程序添加注释并不是一种自觉的行为,我从未见过一个重视代码注释的初学者,也从未见过一个不重视代码注释的经验丰富的程序员。

切割,分解,各个击破

当问题的规模大到难以应对时,解决之道在于将问题分解。分解的方法有两种。第一种方法我们非常熟悉,即将问题分解为若干个部分(如A、B、C),然后各个击破。第二种则不太常见:将问题按照从简单到复杂的顺序逐层分解。对应到App Inventor的编程方法上,就是先添加少量的块来实现简单的功能,并测试其效果,再逐渐过渡到复杂的功能,以此类推。

让我们以第10章中的出题应用为例来具体阐述这两种方法。在应用中,用户可以点击“下一题”按钮浏览问题,也可以检查用户的答案是否正确。从设计角度来说,可以将应用分解为两个部分:问题浏览及答案核对,并针对两个部分单独编程。

但在每个部分中,还可以对整个过程按照从简单到复杂的顺序进行分解。例如,在问题浏览部分,先创建代码来显示问题列表中的第一题,并测试其是否有效;然后编写代码来浏览到下一题,暂时不考虑到达最后一题时可能引起的错误;当测试结果证明可以从头至尾浏览所有问题时,再添加块来处理用户浏览到最后一题的“特殊情况”。

究竟是将问题分解为几个部分,还是按照复杂性分解为若干层,这不是一个非此即彼的问题,却是一个值得思考的问题,关键在于哪种方法更适合你创建的应用。擅于解决此问题的软件架构师非常抢手。

理解编程语言:用纸笔跟踪记录

从是否可见的角度划分,一个应用可以被划分为两部分:可见的部分——最终用户可以看到的应用的外观,包括用户界面上显示的图形及数据;不可见的部分——应用内部的运作机制,就像人类大脑的内部机制一样(谢天谢地!)。在应用的运行过程中,我们既看不到程序中的指令(块),也看不到用于跟踪当前执行指令的程序计数器,更无法看到软件内部的存储单元(里面保存了应用中的变量及组件的属性值)。我们希望的结果是,最终用户仅应看到程序显示呈现出来的东西。然而,对于程序员来说,在开发及测试过程中,我们需要知道应用中正在发生的每一件事情,而不仅仅是显示给最终用户的部分。

开发者在开发过程中所看到的代码都只是些静态视图,因此必须依靠想象力来驱动软件的运行:事件发生了,程序计数器移动到下一个代码块,并执行这个代码块,此时内存单元中的值发生了变化,等等。

编程过程中需要在两种不同的场景之间切换:先从静态模式——编写代码开始,并试着想象程序的实际运行效果;一切就绪后,切换到测试模式——以最终用户的身份测试软件,看它的运行结果是否与预期的结果相一致。如果不一致,必须再切换回静态模式,调整程序,然后再试。如此循环反复,最终获得一个满意的结果。

初学者对于计算机程序的运行机制知之甚少,整个过程看起来就像魔术。依照本教程的指导,应该从简单的应用开始学习(如,点击按钮引发猫叫),再逐渐过渡到较为复杂的应用,而且随着学习的不断深入,或许还可以根据自己的需要,对教程中的例子做出修改。初学者入门后,对程序的内部运行机制有了一些了解,但依然感到对整个过程无法把握。他们经常会说“这个不起作用”或者“它不应该是这样的”。一个程序员最应该具备的能力,是了解程序如何实现那些你主观想象出来的功能,而且要说“我的程序正在做这件事”以及“我的逻辑导致了程序的……”。

了解程序运行机制的一种方法就是剖析一个简单应用的执行过程,在纸上精确地描绘出执行每个代码块时,设备的内部所发生的变化。想象用户触发了某个事件处理程序,然后逐步跟踪并记录代码块的执行结果:应用中的变量及属性如何改变,用户界面上的组件如何改变。就像文学课上的“精读”环节,这样一步一步地跟踪可以迫使你审查语言中的每一个要素(即App Inventor中的块)。

似乎找不到合适的语言来描述一个应用的复杂程度。重要的是你要放慢思路,理清各个代码块之间的因果关系。最终你会明白,那些掌控着整个程序运行过程的规则,并不像最初想象的那样难以理解。

以第8章“总统问答”应用为例(对原教程做了一点修改),如图15-2所示,思考图中的这些代码块。

图15-2 应用启动时,设问题标签的显示文本属性为问题列表的第一项

你能理解这些代码吗?你能跟踪这些代码,并说明每一步都发生了什么吗?

首先跟踪所有相关的变量及属性。画出存储单元的表格,这个例子中,表头分别为当前问题索引值及问题标签的显示文本,如表15-1所示。

表15-1 跟踪程序运行的记录表格

问题标签的显示文本 当前问题索引值

接下来,思考一个问题:当应用启动时,发生了哪些事?不是从用户的角度,而是从应用的内部来分析应用的初始化过程。如果你学过了这些章节,应该见过这些代码,只是没有从内部机制上去思考。当应用启动时:

  1. 设定所有组件的属性值,这些值来自于设计视图中设定的初始值;
  2. 定义变量并设定它们的初始值;
  3. 执行了Screen初始化事件处理程序中的所有代码块。

对程序进行跟踪有助于理解程序的运行机制。那么在完成了应用的初始化之后,表格中应该填写怎样的内容呢?

如表15-2所示,当前问题索引值为1,因为当应用启动时,已经完成了对变量的定义,并将其初始值设为1;而问题标签的显示文本为第一题,因为在Screen1的初始化事件中,选中了问题列表中的第一项,并显示在问题标签中。

表15-2 “总统问答”应用初始化后的变量及属性值

问题标签的显示文本 当前问题索引值
哪位总统在大萧条时期实施了“新政”? 1

下面来跟踪用户点击下一题按钮时发生的事情。代码如图15-3所示。

图15-3 当用户点击下一题按钮时,执行以上代码

逐个检查每个块。首先是变量当前问题索引值的递增。说得更具体些,用户第一次点击下一题按钮时,当前问题索引值为1,经过+1的运算后,将结果2再赋给当前问题索引值,因此,点击按钮后,该变量值为2。接下来看“如果...则”语句,问题列表的长度为3。显然当前问题索引值2小于3,因此“如果...则”语句的判断结果为假,不执行“则”右侧的代码,而是将问题列表中的第2项(第二题)写入问题标签中,如表15-3所示。

表15-3 点击下一题按钮后的变量及属性值

问题标签的显示文本 当前问题索引值
哪位总统在1979年实现了中美建交? 2

跟踪下一题按钮的第二次点击。此时,当前问题索引值已经递增到3,会发生什么呢?继续阅读之前,细心地检查一下,看你能否正确地给出程序的运行结果。

在“如果...则”测试中,当前问题索引值(3)的确大于等于问题列表的长度(3),于是当前问题索引值被设为1,第一题被写入问题标签,如表15-4所示。

表15-4 第二次点击下一题按钮时的变量及属性值

问题标签的显示文本 当前问题索引值
哪位总统在大萧条时期实施了“新政”? 1

我们的跟踪发现了一个错误:最后一题永远也无法显示!怎么解决呢?

当你能够在这个层次上跟踪应用的运行细节时,才能称得上是一名程序员和工程师。你已经摆脱了最初对代码的片面理解,能够透过代码中的那些词汇和语句,去理解编程语言的内部运行机制。诚然,编程语言是复杂的,但机器对每个“词”都有明确而且简单的解释,所以如果理解了代码块与变量或属性变化之间的对应关系,也就掌握了编写或完善应用的方法,当然也就实现了对应用的完全控制。

现在如果你告诉朋友们:“我正在学习编程,让用户点击下一题按钮时,可以翻看到下一道题。这实在太难了!”,他们会以为你疯了。但是这个过程的确很困难,困难不在于概念的复杂性,而在于你不得不有意让自己的脑子慢下来,来搞清楚计算机的每一步处理过程,包括那些你的大脑下意识完成的过程。

应用的调试

一步一步地在纸上跟踪记录程序的运行过程,这种方法不仅有助于我们理解程序,而且在排查程序中的错误时,也是一个屡试不爽的方法。

像App Inventor这样的开发工具(通常被称为交互式开发环境,简称IDE)一般会提供一种调试工具,相当于纸笔跟踪记录的高科技版本,能够自动完成某些跟踪过程,这极大地改善了应用开发的进程。这些工具提供了一个窗口,其中列出了正在运行的程序的相关信息,程序员可以在其中:

  • 在任何一点暂停应用来检查其中的各个变量及属性的值;
  • 单步执行某些指令(代码块)来检查它们的执行效果。

监视变量

在App Inventor中测试应用时,变量及组件的属性值是不可见的。一种变通办法是添加一些用于测试的代码快,在用户界面上用标签来显示变量及属性值,并在测试完成后,删除这些标签及相关代码块。

在App Inventor的早期版本中(App Inventor Classic),可以在编程视图中监视测试过程中的变量及属性值,而不必借助于用户界面中的标签组件。由于这一功能非常有助于应用的跟踪测试,也有助于学习者理解代码,因此该功能将会被移植到App Inventor 2中,请给予关注。

测试单一代码块

在程序运行过程中,除了可以利用监视功能来察看变量及属性值的变化,还有另一个单步执行的工具,可以让程序脱离开正常的运行顺序,单独测试某一个或某几个代码块。右键点击一个块,在快捷菜单中选择“执行该代码块”{![此时需要在开发环境中连接测试设备。——译者注]},这个块就会开始执行;如果这个代码块是一个有返回值的表达式,则返回结果将显示在代码块上方的方框内{![就是代码注释的方框。如果该代码添加了注释,将在注释内容上方插入两行,显示返回值。——译者注]}。

单步测试功能在调试代码块中的逻辑错误时非常有用。再回到刚才总统问答的例子中,测试“下一题按钮”的点击事件处理程序,假设程序中的逻辑错误依然存在——无法浏览所有的问题。当然,你可以在用户界面上点击下一题按钮 ,查看是否每次点击都显示了适当的问题。也可以察看当前问题索引值变量(用标签显示),看每次点击按钮时变量的变化。但是,这样的测试只能检查整个事件处理程序的执行效果。每次点击按钮,事件处理程序中的所有代码块都将被执行,你无法监视到程序运行过程中变量及用户界面的变化。

单步测试功能可以减缓程序的测试过程,并检查任何一个块执行完成后整个应用的状态。一般是从用户界面(屏幕)的初始化事件开始跟踪,直到发现问题为止。在发现无法显示最后一题之后,你可能在用户界面上先点击一次下一题按钮,来显示第二道问题,然后不再点击用户界面上的按钮,而是在编程视图中让整个事件处理程序一步一步地执行。在按钮的点击事件处理程序中,每次只对一个块使用“执行该代码块”,先从第一行代码开始(让当前问题索引值递增),用右键点击该块并在快捷菜单中选择“执行该代码块”。如图15-4所示。

图15-4 使用单步执行工具每次执行一行代码块

此时当前问题索引值变为3,而程序就停在此处,不再继续运行,因为“执行该代码块”只能使被选中的块以及它所包含的子块运行。这样,作为测试者,你可以观察变量及用户界面的变化。接下来,选择下一行要测试的代码块(如果...则),点击右键并选择“执行该代码块”来执行这一组代码。就这样进行单步测试,你会看到每个代码块的执行效果。

单步执行支持渐进式开发

有一点需要强调,这种单步执行指令的方式不仅适用于程序的调试,它同样适用于开发过程中的随时测试。例如,如果你写了一个很长的公式来计算两个GPS坐标之间的距离,可以利用单步执行功能,分步测试这个公式,来验证某些代码块的使用是否正确。

启用与禁用块

App Inventor提供了另一个渐进式的调试工具——禁用代码块。在一段程序中,如果你怀疑某些块可能会出错,可以将其设置为禁用块,或者如果你想单独测试某几个块,可以将其余的所有块禁用,然后再逐步取消对某个或某些块的禁用。在程序运行过程中,这些被禁用的块将被暂时忽略,不参与程序的运行。用这种排除法来测试程序,很快就可以找到那些有问题的代码块。设置代码块的禁用很简单,在代码块上点右键,在快捷菜单中选择“禁用代码块”即可。被禁用的块呈现为灰色,在应用运行时,这些块被忽略。需要时,还可以重新启用这些块,方法是在块上点击右键并选择“启用代码块”。

小结

App Inventor最大的好处在于它的易用性——可视化的编程环境让你可以直接上手创建应用,而不必担心底层细节。但现实问题是,App Inventor不可能知道你的应用要做什么,更不知道如何来做。你可能很想直接进入设计视图与编程视图创建应用,但这里要强调的是,花一些时间来思考并详细准确地设计应用的功能是非常重要的。这听起来有些烦,但如果你能听取用户的想法、创建原型,并采用边开发边测试的渐进式方法,那么创建出精彩的应用便指日可待。