开发体验

第21章 定义过程与代码复用

通常编程语言都会提供一些内置的功能模块,同时允许开发者编写自定义的功能模块,来实现某些特殊的功能,并提高编写程序的效率。App Inventor也不例外。在App Inventor中,那些隶属于组件的紫色代码块,就是编程语言中内置的功能模块,也称为内置过程。同样,App Inventor也允许开发者定义自己的功能模块,即编写自定义过程{![在计算机科学中,过程(procedure)有许多别名,如子程序、函数、方法等,是一个大型程序中的某一部分代码,由一个或多个语句组成,负责完成某项特定任务,或实现特定功能。与其他代码相比,过程是一段独立的代码,有自定义的名称,可以被其他代码多次调用。直接引用过程的名称,就可以实现对过程的调用。——译者注]}。在App Inventor中,通过命名一段代码块来定义过程,从而实现功能的扩展。应用中可以像调用App Inventor中的内置过程一样,调用自定义过程。本章中你将看到,这样将某个具体功能抽象为过程的能力,对于解决复杂问题来说是至关重要的,同时也是创建高质量应用的基石。

在睡觉之前,家长会对孩子说“去刷牙”。“刷牙”的实际含义是“从洗漱柜上拿起牙刷和牙膏,将牙膏挤到牙刷上,在每颗牙齿上刷10秒钟(哈哈!)”,诸如此类。“刷牙”就是一个抽象的概念,代表了一系列的具体指令,而且是一个众人皆知的概念或名称。此时,家长和孩子对刷牙的指令具有相同的理解,让孩子刷牙就等同于让他们完成一系列的具体操作。

在编写程序时,也有相似的对行为的抽象。程序员可以创建一些有名称的指令序列。在一些编程语言中,这些指令序列称为函数(function)或子程序(subprogram);在App Inventor中,称之为过程(procedure)。过程就是一系列的代码块,被赋予了一个有意义的名字,在应用中可以随时随地调用它。

图21-1就是一个过程的例子,它的功能是以英里为单位,计算两个GPS坐标之间的距离。

图21-1 计算两点间距离的过程

不必急于探究这个过程里所包含的细节,重要的是理解这段代码的功能——在已知经纬度的情况下,计算地球表面两点间的距离。任何编程语言中,都不会提供这样的内置功能,这是需要程序员自己来完成的工作,它是对编程语言功能的扩展。如果家长们每天晚上都要向孩子们解释“刷牙”的步骤,那么孩子恐怕到了五年级也不会主动去刷牙。说“刷牙”是一种更有效的方式,至于刷牙的具体时间和具体步骤完全可以因人而异。

同样的道理,在设计或编写一个大型应用时,求两点间距离这个过程一旦定义完成,你就不必再关注它的具体实现方式,只要在需要的时候,直呼其名即可。这种抽象能力对于解决大型问题来说是至关重要的,可以将大型的软件项目分解成若干个便于管理的功能模块。

过程还有助于减少程序中的错误,因为可以消除很多冗余的代码。定义过程只是一次性地将代码规整到一起,之后就可以随时随地调用它。现在假如应用中要计算你所在的位置与其他10个点之间的距离,并求出最短距离。如果没有过程,你可能要复制粘贴10次图21-1中的代码块,而一旦定义了求两点间距离的过程,直接调用它就可以了。此外,像复制粘贴代码块这样的行为,很容易在程序中留下隐患,因为一旦你想修改程序,就必须找到所有复制粘贴过的代码,并逐个以相同的方式修改它们,这种代码的可维护性极差(回顾一下第20章的内容)。想象一下,你试图在一个有1000个块的代码中,找到5~10处曾经粘贴过的代码块!与其被迫地复制粘贴这些块,不如用过程将它们封装起来。

最后,过程将有助于建立代码库,让这些代码在其他应用中也可以被复用。即便是创建一个非常具体的应用,有经验的程序员也会在必要时设法复用其他应用中的部分代码。有些程序员甚至从未创建过自己的应用,他们只是专注于创建可复用的代码库,以便其他程序员可以借此来创建他们自己的应用。

消除冗余

图21-2中的这些代码块来自于“随手记”应用,观察一下这些代码,看能否发现其中冗余的部分。

图21-2 随手记应用中的冗余代码

这里所说的冗余代码指的是遍历列表块(实际上还包括了遍历列表循环内部的块,及其上方的“设笔记标签的显示文本”块)。例子中有三个事件处理程序,都需要在标签中显示笔记本列表的内容:添加新项时、删除某一项时,以及应用启动加载数据之后。

有经验的程序员在看到这样的代码时,脑子里会立即敲响警钟,甚至不必等到开始复制粘贴第一段代码,就能预料到这段代码的复用价值。因此最好将它们封装在一个过程里,这样既保证程序有很好的可读性,也可以让以后的修改变得容易。

因此,经验丰富的程序员会创建一个过程,将冗余的代码搬到过程中,并在原来使用冗余代码的地方调用这一过程。经过这样的调整,程序的运行结果完全一样,但代码更易于维护,也使得其他程序员有机会重复利用这些代码。这种代码(块)的重新整理过程称为代码的重构。

定义过程

我们来创建一个过程,实现图21-2中那些冗余代码的功能。在App Inventor中,定义过程几乎与定义变量一样简单:从内置块的过程抽屉中拖出一个“定义过程...执行”块或“定义过程...返回”块。如果过程需要通过计算返回一个结果,则使用后者(我们将在本章稍后的部分讨论)。

在拖出“定义过程”块后,可以修改过程名称:点击默认名称“我的过程”并输入新名称。由于冗余代码块的作用是显示笔记列表,因此重构时将过程名设为“显示列表”,如图21-3所示。

图21-3 点击“我的过程”为过程命名

下一步是向过程中添加代码块,就是图21-2中的冗余块。将它们从事件处理程序中拖出并放在显示列表块中,如图21-4所示。

图21-4 将冗余代码封装在显示列表过程里

21.3 调用过程

诸如“显示列表”和“刷牙”这样的过程,暗含着要去完成一项任务。不过,在被调用之前,它们起不到任何作用,反过来说,只有在被调用时,才能体现出它们的功能。到目前为止,我们只是创建了过程,却没有调用它。调用它意味着让它运行,或者说让它实现预设的功能。

在App Inventor中,每当定义一个新的过程,就会在内置代码块的过程抽屉里生成一个新的块,如图21-5所示。

图21-5 定义过程后,在过程抽屉中多出一个调用过程块

其实你已经调用过很多过程,就是那些隶属于组件的紫色代码块。例如球组件的“让球移动到指定位置”块,或者短信收发器的“让短信收发器发送消息”块,这些都是App Inventor中的内置过程。当你定义一个过程时,从本质上讲,你是在创建自己的功能模块,这相当于扩展了App Inventor语言的功能,而使用这些块,就是在使用你自己创造的成果。

在“随手记”的例子中,从过程抽屉中三次拖出“调用显示列表”块来取代三个事件处理程序中的冗余代码。例如,列表选择框的完成选择事件处理程序(删除一条笔记),修改的结果如图21-6所示。

图21-6 调用过程等于执行过程内部的代码块

程序计数器

为了搞清楚调用过程块的运行机制,需要想象应用中有一个指针,它指向正在执行的指令,并随着程序的运行而移动。在计算机科学中,这个指针被称作程序计数器。

当程序计数器在事件处理程序中随着执行的指令移动时,一旦遇到一个调用过程块,它就会跳到该过程中,并开始追随过程中块的运行;当过程执行完成,程序计数器将跳回到此前的位置(调用过程块处),并继续向下移动。以“随手记”为例,删除列表项块执行完成后,程序计数器跳到显示列表过程中,并随过程中的块(设置笔记标签的显示文本属性为空,并执行遍历列表循环)移动;最后程序计数器再回到事件处理程序中,继续执行“让本地数据库保存数据”块。

为过程添加参数

显示列表过程将冗余代码封装成一个“动宾词组”,这提高了代码的可读性,使你可以站在更高的层次上理解这些事件处理程序,而忽略掉如何显示列表的细节。这样做的另一个好处是,一旦你想修改列表的显示方式,只需修改一处代码(而不是三处)即可。

就过程的通用性而言,显示列表过程仍然存在局限性。该过程所显示的列表(笔记本)是特定的,用于显示列表的标签(笔记标签)也是特定的,它无法用来显示其他列表,比如应用的用户列表。究其原因,是过程中使用的要素太过具体了。

在App Inventor以及其他编程语言中,与过程相关联的一个重要的概念就是参数,使用参数可以构造出更为通用的过程。为了实现预设功能,过程需要对某些数据进行加工处理,而这些数据就是由参数来提供的。以睡前刷牙为例,可以将牙膏类型和刷牙时间设置为刷牙过程的参数。

在定义过程块的左上角有一个蓝色标记,点击它可以为过程添加参数。对于显示列表过程,我们定义了一个名为“列表”的参数,如图21-7所示。

图21-7 过程将接收列表参数

虽然过程中定义了参数,但过程内部的遍历列表循环仍然针对的是具体列表(笔记本)。我们希望过程能够处理参数传递进来的列表,因此,这里将全局变量笔记本列表替换为参数列表,如图21-8所示。

图21-8 遍历列表循环处理传入的列表参数

新版本的过程具有更好的通用性:显示列表过程可以处理任何传入的列表,并将其显示出来。当你为过程添加参数时,App Inventor将在调用过程块上自动添加相应的插槽,来接收传入的参数。新的带参数的显示列表过程如图21-9所示。

图21-9 调用显示列表过程时,需要为列表参数提供具体的数据

过程定义中引入的参数“列表”被称为形式参数,而调用过程时所提供的具体列表(填充到列表插槽中)被称为实际参数。当应用中需要调用带参数的过程时,必须为每个“形式参数”提供一个“实际参数”,即为每个参数插槽提供一个具体的数据。

对于“随手记”应用来说,“笔记本”列表将作为实际参数,被添加到调用过程块的列表插槽中。经过修改,列表选择框的完成选择事件处理程序如图21-10所示。

图21-10 调用显示列表过程时为其提供实际参数

现在,当调用显示列表过程时,将笔记本列表传递给过程(填充在参数列表插槽中)。此时,程序计数器会跟随过程中正在执行的块,当它遇到参数“列表”时,名义上是在处理“列表”,而实际上是在处理“笔记本”。

由于有了参数,显示列表过程可以用于处理任何列表,而不仅仅是笔记本。例如在“随手记”应用中,假设笔记的内容可以在一组用户中共享,而你想查看一下用户列表,就可以调用显示列表过程,并传入用户列表参数,如图21-11所示。

图21-11 显示列表过程可以用于显示任何列表,而不仅仅是笔记本

过程的返回值

关于显示列表过程的通用性,还有一个问题需要讨论。你能猜到是什么吗?如前所述,它可以显示任何数据列表,但是只能在笔记标签中显示。假如你想用其他界面元素(如另一个标签)来显示列表(如用户列表),该如何是好呢?

一个方法就是重构过程——重新定义它的功能。现有功能是“用指定的标签显示列表”,改为“只返回一段文本“,而这段文本可以被显示在任何地方。为此,需要使用“定义过程...返回”块来取代“定义过程...执行”块,如图21-12所示。

图21-12 定义又返回值的过程块

与“定义过程...执行”块相比,你会发现“定义过程...返回”块的内部有一个额外的插槽,如果将一个变量放入插槽,这个变量就是该过程的返回值,调用过程就等于获取这个返回值。因此,调用过程时,不仅可以向过程传递参数,反过来,也可以获得过程的返回值。

图21-13中是显示列表过程的改进版本,这次使用的是“定义过程...返回”块。注意,由于过程的功能改变了,因此名称也由原来的“显示列表”改为“列表转文本”。

图21-13 列表转文本过程返回一段文本

在图21-13中,局部变量“文本”用来保存遍历列表循环中生成的文本。由于“文本”仅在过程内部使用,因此将其定义为局部变量,而非全局变量。

在之前的显示列表过程里,使用了过于具体的笔记标签组件,来保存并直接显示列表内容。而在改进版本中,用变量文本替代了具体组件,来保存并输出列表的文本。在遍历列表循环执行完毕后,变量文本中包含了列表中的所有项,而且项之间以换行符“\n”分隔(即“项1\n项2\n项3”)。最后,将变量文本插入返回插槽,返回给调用者。

在定义了有返回值的过程后,过程抽屉中同样会自动生成一个该过程的调用块。与无返回值过程的调用块相比,它们在外观上略有差别,如图21-14所示。你可以比较一下两者的不同。

图21-14 右侧的调用块必须填充到另一个匹配的块中

不同的是在“调用列表转文本”块的左侧有一个插头。这是因为在调用该过程时,过程内部将执行一系列指令,并将结果返回给调用块,因此,必须有某个插槽可以接收这个返回值。

在这种情况下,调用块的返回值可以填充到任何一个标签的显示文本属性中。以笔记本列表为例,需要显示列表的三个事件处理程序都可以调用这一过程,如图21-15所示。

图21-15 将笔记列表转换为文本,并显示在笔记标签中

更重要的是,由于在过程里没有关联到任何具体的列表或标签组件,因此过程更具通用性。可以这样说,在应用中可以使用任何标签来显示任意一个列表的内容,如图21-16所示。

图21-16 该过程摆脱了对具体标签的依赖

跨应用的代码复用

通过创建过程,不仅可以在应用的内部实现代码的复用,有些过程,如列表转文本,还可以成为一种通用的资源,被更多的应用所采用。事实上,有许多组织和编程社区致力于创建公共资源,他们在自己感兴趣的领域积累了功能丰富的过程库。

通常编程语言会提供一个导入(import)功能,使得开发者可以在自己的应用中引入其他的代码库。App Inventor目前没有这项功能,因此要想实现过程在不同项目间的复用,只能专门创建一个代码库的应用,其中包含了可复用的过程。当你需要开发一个新的项目时,将代码库应用另存为新项目,并在此基础上进行新项目的开发。

求两点间距离

在显示列表(列表转文本)例子中,我们通过定义过程来消除程序中的冗余代码:你开始写代码,随后发现代码存在冗余,于是整理代码消除冗余,这一过程显得有些被动。作为一个软件的开发人员或开发团队,应该在正式开始编写代码之前,首先对软件进行设计,包括软件中可能需要的过程,以及对这些过程的复用。这样的设计可以在项目开发过程中节省大量时间。

考虑这样一个应用:确定离某人当前位置最近的本地医院。有些应用看似无用,却会在关键时刻派上用场!以下是对这个应用功能的设计。

应用启动时,以英里为单位求两点之间的距离,起点是设备所在的当前位置,终点是最先搜索到的医院。然后再搜索第二家医院,以此类推。在求得若干个距离后,判断距离最短的医院,并显示医院的的地址(以及/或者地图)。

从以上描述中,你能断定这个应用中需要什么样的过程吗?

通常,在一段应用的功能描述中,动词(动宾词组。——译者注)提示了程序中需要创建的过程以及过程的功能,而描述中重复的部分则是另一类线索(“诸如此类”之前的描述)。这种情况下,“求两点之间的距离”以及“判断出最短距离”成为两个必需的过程。

现在考虑设计一个过程“求两点间距离”(名称贴切就好,要做到独出心裁可不是我的强项)。在设计过程时,首先要确定过程的输入及输出。所谓输入,就是向调用过程块传递参数,来实现过程的功能;所谓输出,就是过程要向调用者返回执行结果。在这里,调用者需要向过程传递两个点的经度及纬度值,如图21-17所示;而过程的任务是以英里为单位返回两点之间的距离。

图21-17 调用块输入四个参数,并收到返回的距离

图21-18中显示了我们在本章开始时提到的那个过程,使用公式求得两个GPS坐标点之间的近似英里数。

图21-18 求两点间距离的过程

图21-19中显示了对上述过程的两次调用,每次都会求出当前位置与指定医院之间的距离。

图21-19 两次调用求两点间距离的过程

第一次调用中,起点为用户当前所在位置,实际参数为位置传感器的经纬度值,终点是圣玛利亚医院,对应的实际参数暂时由两个全局变量提供,计算的结果保存在变量“距离_圣玛利亚医院”中。第二次调用也类似,只是将终点改为加州医疗中心。

接下来程序比较求得的两个距离,来判断哪个医院最近。不过这种处理方式适用于仅有两家医院的情况,假如还有更多的医院,就需要创建一个距离列表,将所有求得的距离添加到列表中,通过遍历列表循环来比较所有距离,并找到最小值。依你所学,你能写出这个过程吗?试试看,将其命名为“求最近距离”,以距离列表为参数,并返回最短距离在列表中的索引值。

小结

像App Inventor这样的编程语言提供了一系列最基本的内置功能模块。为了扩展App Inventor语言的功能,可以编写自定义过程,将新的功能抽象成一个恰当的名称。虽然App Inventor本身并没有提供“显示列表”功能,但是你实现了这一功能。计算两个GPS坐标之间的距离,这个功能也要经常用到,怎么办呢?答案是靠你自己来创造。

我们这里创建的过程只是一些练习。在实际工作中,想要创建大型的、可维护的软件,还要定义更高级别的过程(在一个过程中调用另一个过程,甚至多重调用。——译者注),这将有助于解决更为复杂的逻辑问题,使你的思路免于陷入细节之中。对于一个程序员来说,这是一种至关重要的能力。过程是将代码块封装起来,并赋予它一个贴切的名字。在编写过程时,你会关注这些代码块的细节,但对程序的其他部分而言,这个过程只是一个抽象的名字,你可以站在更高的层次上来理解并使用它。