第十四章 简易家庭帐本——分类汇总及其他
在上一章我们实现了账本应用的年度收支汇总功能,采用表格及图表(折线图)两种方式显示年度收支汇总数据。本章将实现其余六项功能——年度及月度收支的分类汇总,包括四项年度分类汇总,两项月度分类汇总,如图14- 1所示,其中前三项的数据显示方式为两种——表格及图表(饼状图),后面三项只显示数据表格。此外,本章还将完成应用的最后一项功能——使用说明,并对开发过程作简短的总结。
在上一章中我们已经完成了统计汇总的功能说明,并完成了用户界面设计等内容,本章将略去这些内容。本章共分八节,其中第一节为技术准备,讲解饼状图的绘制方法;第二至六节分别讲解六项分类汇总功能的实现;第七节实现使用说明功能,最后一节对整个帐本应用进行总结,并给出改进思路。
第一节 技术准备——绘制饼状图
我们以“个人收入汇总”为例,来讲解饼状图的绘制方法。
一、数据模型
1、原始数据
如图14- 2所示,通过对指定年份的收入数据进行统计汇总,可以得出每位家庭成员某一年度的收入合计,我们将利用图中的样例数据讲解与绘图有关的数据模型。
2、绘图数据
我们要绘制的饼状图如图14- 3所示,其中包含了下列数据要素: (1) 总量:全体成员的收入总和,用于计算个人收入的百分比; (2) 分量:每个收入者的年度收入合计占总量的百分比,用Pi表示,i为自然数,最小值为1,最大值为家庭成员的人数,下同; (3) 收入者:家庭成员的姓名。
从以上数据要素推算出绘图要素,包含下列内容:
- 绘图起点:以3点钟方向(把饼图想象成钟表表盘)为0度,顺时针为正(角度增加);
- 扇形半径:采用固定值作为扇形半径,用R表示,这里设R=100像素;
- 扇形角度:用Di表示,计算公式为:Di = Pi * 360,单位为角度;
- 扇形起始角度:用Ai表示,A1=0,A2=A1+D1,… Ai=Ai-1+Di-1 ;
- 扇形终止角度:分别为A2、A3、…、A1;
- 标注半径:用RS表示,先假设为R的1.25倍,即 RS = 1.25 * R ;
- 标注角度:用ASi表示,为扇形的中央,ASi = =Ai-1+Di / 2;
根据样例数据,可以计算出上述绘图要素,具体的计算结果如表14- 1所示。 表14- 1 绘制饼状图的样例数据
将上表中的数据以列表的方式组织起来,以便程序调用,如图14- 4所示。注意列表的结构,一级列表的长度与家庭成员的人数对应,二级列表为键值对列表,包含5个键值对(列表长度为5),对于不同的统计汇总项目,键值对中的“键”是一致的。
二、绘图方法
1、画线
App Inventor不能直接绘制扇形,我们利用画布组件的画线功能,自圆心向圆周画若干条线段,线段的长度为半径,线段之间的角度差为某个固定值(如0.5或1度)。如图14- 5所示,设圆心坐标为(x0,y0),圆的半径为R,我们试图在0到60度之间画一个扇形。假设每隔1度画一条线,则一共要画60条线,每条线的起点为圆心(x0,y0),终点为圆周上的点(x,y),它们之间的关系可以用下列公式描述: x = x0 + Rcosθ y = y0 + Rsinθ 其中θ为角度值,取值范围:0≤θ≤60。
在App Inventor中,利用上述公式绘制线段的代码如图14- 6所示,其中0≤角度≤60。这段代码是从一个循环语句中拖出来的,其中的“角度”为循环语句的占位变量(简称循环变量),这段孤立的代码已经超出了循环变量的有效范围,因此图中用带有感叹号的红色三角形(App Inventor警告信息)表示该代码块非法。
2、画扇形
利用循环语句,让图14- 6中的代码重复执行,通过设置画笔的线宽以及循环变量“角度”的增量,我们可以改变绘制扇形的效果。这里我们分别设画笔线宽为1、2、3,角度增量为0.5与1,并测试不同条件下的绘图效果,代码如图14- 7所示,测试结果如图14- 8所示。
我们利用屏幕的标题属性来显示画笔线宽以及角度增量,对照六个测试结果,我们认定当画笔线宽为3,且角度增量为0.5时,绘制效果最佳。
当然,如果令画笔线宽为1,角度增量为0.1,也可以实现很好的绘图效果,但循环次数将增加到300次,这会延长程序的运行时间,也就是增加用户的等待时间,因此我们需要在视觉效果与运行效率之间求得平衡。
3、画饼图
首先我们对“画扇形”进行改进,缩小合成颜色的取值范围(丛原来的1~200改为50~200),以避免扇形的颜色过深或者过浅;另外,调整圆心的位置为(150,150),将半径改为100,并去掉测试用的代码,修改过的“画扇形”过程如图14- 9所示。
然后定义一个“数据标注”过程,代码如图14- 10所示。用黑色文字标注收入者姓名及收入百分比。(你可能好奇,为什么不能将“文字”、“\n”及“比例”拼成一个字串,这样仅需调用一次画布的“写字”过程,而且“比例”的y坐标增加不受字号大小的限制。我尝试过,不成功。)
接下来定义“画饼图”过程,利用循环语句对图14- 4中的列表数据进行遍历,通过调用“画扇形”及“数据标注”过程,完成饼状图的绘制,代码如图14- 11所示。
最后在按钮点击事件中调用“画饼图”过程,代码如图14- 12所示,测试效果如图14- 13所示。
我们已经利用一组假想的数据,完成了饼状图的绘制,下面将针对具体的分类数据进行数据的整理,并绘制相应的数据表格及图表(饼状图)。
第二节 年度收入分类汇总
一、数据模型
1、原始数据
收入分类汇总的前提是从数据库中读取全部的收入数据,从中筛选出指定年份的收入记录,并按照收入类别进行分类汇总(求合计值),最后求得的结果应该如图14- 14所示。图中的全局变量“汇总项集合”是收入类别列表,用来帮助我们理解原始数据中各个数据项的含义。实际上我们的程序中并不需要“原始数据”及“汇总项集合”这两个全局变量,我们将创建一个有返回值的过程,来求出“原始数据”列表,这里只是利用全局变量的名称,来表明数据的内容。
2、制表数据
“制表数据”来源于“汇总项集合”及“原始数据”两个列表,将汇总项集合与原始数据中的列表项两两配对,每两对合并为“制表数据”列表中的一个列表项,并添加表头及表脚,制表数据的格式如图14- 15所示。从图中可以看出,数据表格包含4列,共有4个数据行,外加表头、表脚各一行。与“原始数据”一样,我们的程序中并不需要全局变量“制表数据”,我们将创建一个有返回值的过程来求得“制表数据”,这里只是利用全局变量“制表数据”的名称,来说明数据的内容。
3、绘图(饼状图)数据
绘图数据的格式如图14- 4所示,不同的汇总项其绘图数据列表的结构相同,只是列表长度以及列表项的具体内容不同。我们将创建一个有返回值的过程“绘图数据”,来求得绘图数据列表。
二、页面逻辑
- 当用户在“汇总项目选框”中选择了“年度收入分类汇总”时,隐藏月份选框,汇总按钮的显示文本为“汇总”,此时用户可以在年份选框中选择汇总年份,并点击汇总按钮;
- 当用户点击了“汇总”按钮后,默认显示4列带有表头及表脚的数据表格,汇总按钮显示文本改为“显示图表”,此时用户可以点击“显示图表”按钮,查看数据所对应的饼状图;
- 当用户点击了“显示图表”后,屏幕上显示汇总数据的饼状图,汇总按钮的显示文本改为“显示数据”,此时用户可以点击“显示数据”,查看数据的表格形式。
三、编写代码——创建过程
1、原始数据
这是一个有返回值的过程,返回值为一级列表,每个列表项对应于每个收入类别的全年收入总和。代码如图14- 16所示。该过程同样适用于其他的年度分类汇总功能。
为了更好地理解上述过程,这里对各个参数以及局部变量的含义给出解释:
- 选中年:年份选框中的选中项;
- 全集:针对年度收入分类汇总而言,全集即收入全集;
- 汇总项集合:收入类别列表,含工资、奖金等8个列表项;
- 键:对年度收入分类汇总而言,键为“类别”;
- 原始数据:将要返回的一级列表,其中每个列表项为选定年度某一类收入的合计值;
- 数据年:在遍历收入全集时,每条收入记录(键值对列表)中“日期”所在的年份;
- 数据值:与参数“键”所对应的值,对于年度收入分类汇总而言,这个值可能是工资、奖金等8项中的任何一项;
- 汇总项数:参数“汇总项集合”列表的长度,对于年度收入分类汇总而言,该值为8;
- 筛选值:参数“汇总项集合”列表中的某一项;
- 当前值:原始数据列表中某个列表项的当前值;
- 增加值:遍历收入全集时,某条记录中的金额值。
为了测试该过程的返回结果,我们在汇总按钮点击程序中添加一个条件语句,根据“汇总项目选框”的选中项索引值来决定所要执行的程序,代码如图14- 17所示。
上述代码中利用返回按钮的显示文本属性,来显示原始数据的返回值,为了完整地显示全部列表项,暂时设置返回按钮的高度为60像素,测试结果如图14- 18所示。稍后记得将设置高度的代码删除。
2、制表数据
如图14- 19所示,“制表数据”过程有四个参数,而且与“原始数据”过程的参数完全相同。参照图14- 15的列表结构,首先为局部变量“制表数据”添加表头,然后同时对“汇总项集合”与“原始数据”两个列表进行遍历(两个列表的长度相等),并先后将两个列表中相对应的列表项添加到局部变量“二级列表”中,当二级列表的长度等于4时,将二级列表添加到“制表数据”列表中,并清空二级列表;或者,虽然二级列表长度不等于4,但是循环语句已经遍历到了列表的最后一项,此时为二级列表添加两个空字符列表项,然后再将二级列表添加到“制表数据”中;在遍历列表过程中,累计“原始数据”中每一项的值,并保存在局部变量“总计”中;最后,为“制表数据”添加表脚——总计值。
我们将汇总按钮点击程序稍加修改,就可以测试“制表数据”过程的返回值,这一次我们不必借用返回按钮的显示文本属性,而是直接调用“绘制表格”过程,并将该过程的参数设为“制表数据”过程的返回值,代码如图14- 20所示,测试结果如图14- 21所示。
3、绘图数据
如图14- 4所示,绘图数据的结构为三级列表,一级列表的长度等于“汇总项集合”列表的长度;二级列表为键值对列表,如图14- 22所示。
图中的键值对列表包含5个键值对,其中的“键”是固定不变的,值分别来源于“汇总项集合”以及对原始数据的计算,“绘图数据”过程的代码如图14- 23所示。代码中包含两个独立的循环语句,第一个循环语句是针对“原始数据”列表的循环,用来计算收入总计;第二个循环语句同时遍历“汇总项集合”与“原始数据”两个列表,为绘图数据设定标注文字,利用前面所得的总计值来求得各项绘图数据——标注比例、起始角度、终止角度以及标注角度;将所得的各项数据组织成键值对列表,并添加到局部变量“绘图数据”中。注意对“起始角度”的设置——放在第二个循环语句的最后一行。
为了测试“绘图数据”过程的返回值,我们需要将“画饼图”项目中的代码复制到帐本项目中,利用App Inventor新增的代码背包功能,将“画饼图”项目中的三个全局变量R、X0、Y0以及三个过程“画扇形”、“数据标注”、“画饼图”放置到代码背包中,并在帐本项目中将它们提取出来。复制过来的代码中与组件名称“画布1”相关的语句上有警告标记,将“画布1”修改为“画布”,这些警告标记就消失了。我们需要对“画饼图”过程加以改造,首先,为过程添加四个参数——选中年、全集、汇总项集合及键;其次,添加清除画布语句,并设画布的画笔线宽为3;然后添加局部变量“绘图数据”,并设置其值为“绘图数据”过程的返回值;最后,将循环语句中对全局变量“绘图数据”的遍历修改为对局部变量“绘图数据”的遍历,代码如图14- 24所示。
依然利用汇总按钮的点击程序来测试“绘图数据”过程的返回值,代码如图14- 25所示。
测试过程中发现,当汇总按钮显示“显示数据”时,点击汇总按钮后,画布上除了显示数据表格外,还有部分饼状图残留在画布上,为此,我们需要在上一章已经完成的“绘制表格”过程中添加一行代码——清除画布,代码如图14- 27所示。
四、编写代码——事件处理程序
我们已经在测试上述过程的返回值时,改写了汇总按钮的点击程序,这里对该程序进行一点改进,以便代码具有更好的适应性,代码如图14- 28所示。
上述程序在调用“画饼图”及“绘制表格”过程时,都需要提供“选中年”参数,这里将原来的“2016”修改为年份选框的选中项。汇总按钮点击程序还存在进一步改进的空间,读者不妨随着我们的进度,思考如何修改才能提高代码的复用性,并改善程序的结构。
在测试过程中存在两个问题,一是绘制饼图时需要等待的时间较长,二是饼图中各个扇形的圆心并没有汇聚在同一个点。前者的原因是绘图过程涉及到大量运算,以及处理器与显示设备之间的数据通信;后者的原因在于画笔的线宽,如果画笔线宽为1,则各个扇形的圆心将重合在同一个点。
第三节 年度个人收入汇总
上一节中的三个过程——原始数据、制表数据以及绘图数据,同样适用于本节的功能,只是在调用过程时提供的参数有所不同,因此我们只需修改汇总按钮点击程序,即可实现本节功能,代码如图14- 29所示,代码的测试结果如图14- 30所示。
测试发现饼状图存在一个1%的空白,因为三个家庭成员收入比例的总合为99%。究其原因,可能是计算比例时四舍五入运算的结果,当两个百分比的小数位均小于0.5,但其小数位之和大于0.5时,就会导致最终的百分比之和不足100%。解决问题的思路有两种,如果软件本身注重数据的精确性,则可以保留1位小数,不过即使这样,最终还是有可能比例之和不足100%(可能是99.9%),因此,还有另外一个思路,即,让最后一项百分比等于100%减去此前各项百分比之和,这样可以确保百分比的总和为100%,在这样的前提下,再考虑数据的精确性,来确定保留小数的位数。
我们来修改绘图数据过程,添加一个局部变量——比例之和,让最后一项百分比等于100与前面各项百分比之和,代码如图14- 31所示,测试结果如图14- 32所示。
第四节 年度支出分类汇总
你可能已经发现,要实现各个年度分类汇总功能,只要在汇总按钮点击程序中添加新的分支就可以了,并不需要修改此程序之外的任何代码。不过,随着汇总项目的增多,汇总按钮点击程序中的分支越来越多,程序显得冗长而混乱,为了让程序简洁清晰,我们需要对程序进行改造。首先改造“汇总项目选框”的完成选择程序,如图14- 33所示,将“设global年度收支汇总”的语句从汇总按钮点击程序中转移到完成选择程序中。图中红色方框内的代码为新增部分。
然后添加两个过程——“显示分类汇总数据”与“显示分类汇总图表”,把汇总按钮点击程序中的部分代码转移到这两个过程中,并设置三个参数——“全集”、“汇总项”以及“键”,替换原有代码中的具体值,过程代码如图14- 34所示。
然后再创建两个过程——“显示数据”与“显示图表”,来调用上面两个过程,代码如图14- 35及图14- 36所示。
最后改造汇总按钮点击程序,外层条件语句中包含两个分支——显示数据分支与显示图表分支:在显示数据分支中(当汇总按钮的显示文本不等于“显示图表”时),调用显示数据过程,实现了前四个汇总项目的表格绘制功能;在显示图表分支中,调用显示图表过程,实现了前四个汇总项目的图表绘制功能。改造后的汇总按钮点击程序如图14- 37所示。
注意:在显示数据分支中包含了一个条件语句:仅当选中前四个汇总项时,应用才具有绘制图表功能。上述代码的测试结果如图14- 38所示。
上图中表格数据的绘制结果还算令人满意,但饼状图的效果有点差强人意,问题出在那些比例为0或接近于0的成分上,这些成分的标注文字挤作一团,难于分辨。解决这一问题的思路是,对“标注比例”进行筛选,对比例为0的成分不予标注,当比例小于3%时,只标注文字,不标注比例。在“绘图数据”过程中实现我们的改进,代码如图14- 39所示,测试结果如图14- 40所示。
第五节 年度专项支出汇总
专项支出汇总功能与前面几项分类汇总功能略有差异,首先,该功能不需要绘制饼状图,因为各个专项的支出合计与专项支出总和之间不存在可供参考的总量与分量之间的比例关系;其次,专项支出中与参数“汇总项集合”相对应的数据(读取自数据库)不是简单的一级列表,而是一个键值对列表,因此需要从键值对列表中提取专项名称列表,作为“汇总项集合”参数的值。为此我们需要一个有返回值的“专项名称列表”过程,代码如图14- 41所示,我们利用代码背包将该过程从QUERY屏幕复制到SUMMARY屏幕中。
下面修改“显示分类汇总数据”过程,为参数“汇总项集合”添加一个“如果…则…否则”语句,代码如图14- 42所示。
最后,修改“显示数据”过程,增加一个“否则…如果”分支,代码如图14- 43所示,上述代码的测试结果如图所示。
注意测试结果中汇总按钮的显示文本没有改变,再次点击该按钮时,依然执行显示数据功能。
第六节 月度收入、支出分类汇总
对月度数据的分类汇总只涉及绘制表格,不需要绘制图表。我们来回顾一下年度收入、支出分类汇总的相关代码,看看哪些代码是与年份相关的,并对这部分代码进行改造,以适应对月份的汇总需求。从汇总按钮点击程序开始,如图14- 45所示,我们只关心与显示数据相关的代码。
上图中程序调用的终点是“原始数据”过程,如图14- 16所示,它完成了对数据的汇总运算,并返回一个一级列表。原始数据的格式如图14- 2所示,它是我们后续绘制表格及图表的依据。我们对原始数据过程进行修改,通过判断“月份选框”是否可见,来决定是否对数据进行月份的筛选。修改过的代码如图14- 46所示。
上图中共有三处修改:
- 添加局部变量“数据月”,并设置其初始值为0;
- 在“设数据年”之后,添加条件语句,如果月份选框可见,则求“数据月”的值;
- 将原有对年份的筛选扩展到对月份的筛选:当月份选框可见时,同时对年份及月份进行筛选。
下面修改“显示数据”过程,以实现月度收支分类汇总功能,代码如图14- 47所示。
从上图中可见,我们并没有增加新的条件分支,只是在索引值=2(年度收入分类汇总)及索引值=4(年度支出分类汇总)的分支中,增加了一个“或者”块,将分支条件分别扩展到索引值=5(月度收入分类汇总)及索引值=7(月度支出分类汇总)。上述代码的测试结果如图14- 48所示。
以上我们完成了全部的统计汇总功能,这个部分占用了两章的篇幅,难点在于利用画布组件绘制数据图表,不过这也是整个应用中最有趣的部分。现在距离整个应用的完成还差一步——实现使用说明功能。
第七节 使用手册
可以说到目前为止,我们已经实现了家庭帐本的全部功能:登录、导航、收支录入、查询、汇总以及系统设置等等,有了这些功能,用户可以实现对收支信息的管理,我们的目标已经实现了。但是,作为一款将要正式发布的软件产品来说,还需要提供一份详细的用户手册,也就是我们导航菜单中的“使用说明”,来指导用户充分地利用软件的各项功能,减少使用过程中的误操作,如果可能,还要给出软件出现故障时的处理方法。
使用说明的内容全部是文字,在App Inventor中,用于显示文字的组件有标签、文本输入框等,但这些组件用于显示有结构的文本(多级标题)时,存在许多问题。首先,在一个组件中,无法设置不同类型文本的属性,举例来说,我们希望文本中标题采用大字号的粗体字,而内容采用普通文字,这样的需求无法在一个标签中实现。其次,使用说明中内容篇幅较长,如果对内容不加以分割,势必给用户的阅读造成不便,我们希望在屏幕顶端设置一个文档目录,点击目录项可以直接到达具体内容,这样的功能用标签或输入框组件无法完成。鉴于上述原因,我们决定用web浏览框组件实现使用说明功能。
一、用户界面设计
打开HELP屏幕,设置其标题属性为“简易家庭帐本_使用说明”;向用户界面中添加一个Web浏览框组件,并设置其首页地址属性为“file:///mnt/sdcard/AppInventor/Assets/helpPage.html” (稍候解释这项设置),宽度、高度属性为充满,只勾选“允许显示”属性,如图14- 49所示。
二、编辑并上传HTML文档
剩下的事情就是编辑一个文档,即web浏览框组件首页地址属性所对应的文档。文档的格式如图14- 51所示。这是一个HTML文档,关于HTML及其与之相关的内容,可以访问W3C的官方网站(http://www.w3school.com.cn/),进行更深入的学习,这里不作详细介绍。文档的完整内容将以附件的方式添加到本章的结尾。
在App Inventor设计视图中,上传该文件,在图14- 49中我们已经完成了文档的上传。
三、测试
使用说明功能的实现,不需要编写任何代码,主要工作量集中在编写html文件上。现在我们可以直接进入测试环节,测试结果如图14- 52所示。
上图中自左向右的顺序,也是我们测试操作的顺序,分别为:
- 屏幕初始化后,web浏览框组件显示使用说明文档的目录,以及第一项说明(设置密码)的标题及部分内容,此时用户可以向上划动屏幕,按顺序浏览相关内容,也可以点击目录中的任何一项链接,直接到达相关内容;
- 点击目录中的“收入记录”项,直接到达“收入记录”的标题;
- 用户向上划动屏幕,可以浏览“收入记录”的具体内容;在收入记录内容的结尾处,有“返回目录”链接,用户可以继续向上或向下划动屏幕,查看其它内容,也可以点击“返回目录”链接,返回到目录区;
- 返回目录区后,发现刚刚点击过的“收入记录”文字颜色有变化,说明已经被点击过了。
注意:我们现在设置的web浏览框首页地址属性只能用于在AI伴侣中进行测试,当整个应用开发完成之后,需要将项目编译成apk文件,安装到安卓设备上,在开始编译之前,需要将web浏览框的首页地址属性修改为“file:///android_asset/helpPage.html”,应用才能访问到该文件。
第八节 开发心得及改进思路
我们用七章的篇幅完成了帐本应用的开发过程讲解,此时此刻,真的希望就此罢手,给自己好好放个假,不过俗话说,编筐织篓,全在收口,即便是对于我本人来说,将开发过程中的点滴体会记录下来,也是一件非常重要的事情。
一、开发心得
1、关于命名
在应用开发过程中,我们无时无刻不在与命名打交道。首先是为应用命名,“家庭帐本”,还是“简易家庭帐本”,这是一个问题(听起来有点儿像是莎士比亚戏剧中的某一段)!其次,为屏幕命名——那些至高无上的一旦生成便不可更改的大写字母(简直就是宿命)!接下来是为组件命名,就像给自己的孩子起名一样,对它们的未来充满期待!然后是为变量命名,由于每次引用变量都要捎带上那个蹩脚的“global”,因此不得不尽量缩短名称的长度。最后是为过程命名,用名词,还是动宾词组,这又是一个问题!至于应用中加载的那些文件,它们的名字也是命名,不过我们暂且将它们忽略不计!
道德经中说,“无名,万物之始,有名,万物之母。”名字这件事太过司空见惯了,我们甚至从未留心过它们存在的意义,然而,当你走上编程之路后,它开始时时困扰你,尤其是那些学习过代数和解析几何的同学,习惯了用x、y进行思维,而且对ABC情有独钟,当你带着这些偏好开始编写程序时,噩梦就开始了,你的残存的一点精彩的思路被这些毫无意义的符号淹没了!
对于编程而言,我们学习和使用的是一种语言,语言的作用是表达和交流,因此它的每一个要素要有意义,我们要用有意义的符号来显式地表达我们思想的痕迹,让我们的思维建立在稳固的、清晰的概念之上,而命名正是构造这些概念的开始。如果最终你的代码读起来像诗歌,或者像一段缜密的推理,那一定是你的命名恰如其分!
其实,编程中的命名远不止我们在第一段中列举的例子,在我们的帐本应用中,有很多键值对列表,其中的“键”是一种命名,此外,我们向数据库中保存数据时,那些“标记”同样也是一种命名。
在整个帐本的开发过程中,实际上存在着命名的失误。不知你是否留意到,在收入记录中有“收入类别”一项,它既是预设项“收入类别”列表的名称,同时也是向数据库保存或提取数据时使用的“标记”,此外,它还应该是收入记录的键值对列表中的“键”。然而不幸的是,我们在键值对列表中的键采用的是它的简化版本——“类别”。这样的简化充其量可以减少图形化代码的宽度,以利于控制截图的大小,或者为每条记录减少2个字节的存储空间(这一点空间相对于存储设备的容量来说,就像大海中的一滴水!)。然而,它带来的麻烦却是显而易见的。如图14- 16中,原始数据过程有四个参数,如果我们当初将“收入类别”作为键值对列表的“键”,那么这里就可以节省一个参数,让“汇总项集合”与“键”合二为一。(虽然不是所有“汇总项集合”都能与“键”合二为一,例如“收入者”与“家庭成员”,但这是唯一的一个例外,我们可以用条件语句加以处理。)
除了“收入类别”,还有“支出一级分类”,我们不得不为这样的命名付出代价——为程序添加更多的条件语句。
以上例子让我们有所醒悟,即,命名的一致性是命名的一个重要原则,同一个事物,同一个名城!这个是教训。
2、关于过程的命名
收支汇总功能(SUMMARY)是包含过程最多的一个屏幕,让我们回顾一个图14- 45——程序之间的调用关系:汇总按钮点击程序①显示数据②显示分类汇总数据③制表数据④原始数据⑤年、月,这是一段典型的包含多重调用关系的例子,共有五层调用。从过程的名称上,大致可以推断出某个过程是否有返回值。这些名称可以分为两类——名词及动宾词组,那些以名词为名称的过程是有返回值的,而以动宾词组为名称的过程是没有返回值的。读者不妨将SUMMARY屏幕中的过程检查一遍,看看上述归纳是否存在例外。如果有例外,那是我的失败!
此外,在这些逐层调用的过程之间,第③层的“制表数据”过程是一个分水岭,从过程的名称上看,它之前的两个过程,从①②体现了一般与特殊的关系(抽象与具体的关系);它之后的过程,从④③体现了粗与精的关系,或者说原料与成品的关系。
于是我们斗胆给出以下结论:对多重调用的过程来说,无返回值过程间调用的顺序,是抽象过程调用具体过程;有返回值过程间调用的顺序,是具体过程调用抽象过程。这里的具体与抽象指的是过程名称之间的相对关系。
我们不必为自己的以偏概全而感到羞愧,这是我们进步的阶梯,是迈向真知的第一步。有了这样的结论,我们可以从两个方向来验证它,一是寻找肯定的例子,二是寻找否定的例子,而最有效的验证方式是寻找反例!让我们以此为起点,留心那些多重调用的例子,并为上述结论寻找反例。
在我们发现反例之前,不妨以上述结论为指导,构造我们的复杂程序,无论是从抽象到具体,还是从具体到抽象,只要我们有自觉,就会有收获。
关于过程的命名有两点心得:
- 用名词为有返回值的过程命名,用动宾词组为无返回值过程命名;
- 在多重调用的过程之间,无返回值过程间的调用顺序为从抽象到具体,有返回值过程间的调用顺序为从具体到抽象。
3、变量,还是过程
细心的读者会发现,所有变量的名称都是名词,或名词性的词组,如上所述,有返回值过程的名称也是如此,那么这两者之间有哪些共同点?又有哪些差别?它们之间有怎样的关系呢?
它们的共同之处是,它们之中都包含了数据,或者说,我们可以从中读取需要的数据。而变量名称或过程名称就是这个数据的代号。
它们之间的差别是,变量的值保存在设备的内存中,可供其他程序多次调用;过程的返回值是在调用过程时才生成,使用一次后,返回值被销毁,不占据内容空间。
从原则上讲,它们之间可以相互转化,它们之间的关系是:
有返回值过程 = 全局变量 + 无返回值过程
在编写程序过程中,究竟应该采用哪种策略,要视具体问题而定。本章中有三个新创建的有返回值过程——原始数据(图14- 16)、制表数据(图14- 19)及绘图数据(图14- 23),在涉及绘制饼状图的功能中(年度收入分类汇总、年度个人收入汇总、年度支出分类汇总),需要调用这三个过程,它们之间的调用关系如图14- 52所示,其中的“原始数据”过程被其他两个过程各调用一次,也就是说,在实现同一个汇总功能时,“原始数据”过程被调用了两次,这两次调用的返回值是完全相同的。
从程序运行效率的角度考虑,像“原始数据”这样的有返回值过程,应该采用“全局变量+无返回值过程”的策略,这样只需执行一次“原始数据”过程,节省了程序运行时间。比较这两种策略的优劣: 有返回值过程:不占用内存,但计算次数多(时间换空间); 全局变量+无返回值过程:占用内存,但计算次数少(空间换时间)。
究竟如何取舍,是节省内存更重要,还是计算效率更重要,这同样是一个莎翁问题。不过,就一般的简单应用而言,也许两者都不重要。大体的原则是,当一个运算结果需要被多次调用时,建议使用全局变量策略,这样有利于节省计算资源(减少CPU的处理量);相反,如果一个计算结果仅需要在一处调用,则建议使用有返回值过程,这样不仅可以节省内存资源,也可以使得代码更加简洁。
二、改进方法及思路
1、用“全局变量+无返回值过程”替代“有返回值过程”
如上所述,“原始数据”过程更适合于采用“全局变量+无返回值过程”策略,这一改进需要以下五个步骤:
(1) 声明一个全局变量“原始数据”,将原来的有返回值过程改造成无返回值过程,修改后的代码如图14- 52所示。注意过程的名称由名词变为动宾词组。
(2) 改造“汇总项目选框”的完成选择程序
代码如图14- 54所示,将部分代码转移到汇总按钮点击程序中。
(3) 改造汇总按钮点击程序
一旦用户点击了汇总按钮,则调用“求原始数据”过程,以便设置全局变量“原始数据”的值。代码如图14- 55所示。
(4) 改造“制表数据”过程
如图14- 56所示,删除原有的局部变量“原始数据”,并用全局变量“global原始数据”替代原来的局部变量。
(5) 改造“绘图数据”过程
如图14- 57所示,删除原有的局部变量“原始数据”,并用全局变量“global原始数据”替代原来的局部变量。
2、预设项信息与收支记录信息之间的关联
目前的收入及支出记录中,对于预设项信息的引用,直接使用的是预设选项列表中的值,例如,收入记录中的“收入者”信息来自于数据库中保存的“家庭成员”列表,当用户在系统设置功能中修改了某个家庭成员的姓名时,已经输入的收入信息中“收入者”一项的内容并没有改变,这样会导致这部分信息成为僵尸信息,查询全集信息时(按起止日期进行筛选),可以找到它们,但是以收入者为筛选条件时,这些信息永远都无法被查询出来,而且在统计汇总功能中,这部分收入信息也被忽略掉。
这或许是NoSQL数据库本身的局限。在SQL数据库中,这些预设项信息以表格形式保存,如表14- 2所示。数据表中包含两个字段,三条记录。其中的字段ID通常是自动递增的整数,具有唯一性,即便某一条记录被删除,新增的记录也不会覆盖已经删除的ID。
表14-2 关系型数据库中家庭成员信息的保存方式
另一方面,在SQL数据库中,收入记录中“收入者”一项中保存的数据是家庭成员的ID,而非具体的文字(家庭成员的姓名),如表14- 3所示。表格中所有的预设选项字段,引用的都是对应的ID值,而非具体名称,这样,即便用户修改了家庭成员的姓名,比如,将“张老三”改为“张三”,收入记录中的收入者仍然指向同一个人(ID不变)。
表14- 3 关系型数据库中收入记录的保存方式
SQL数据库中的这种数据存储方式给了我们一些提示,NoSQL数据库中虽然不能实现ID的自动递增,但这种思路是可以借鉴的。
3、用户可以修改“收入类别”及“支出一级分类”
应用现有的系统设置功能中,并没有设置收入类别及支出一级分类功能,但是对于用户而言,他们可能对分类方式有自己的见解或偏好,眼下这种固定的分类很难满足用户个性化的需求。不过有一点需要提醒开发者,正如我们在第2个改进思路中所说,修改分类信息可能导致部分已经输入的收支信息成为“僵尸信息”,这正是第2个改进思路中要解决的问题。
4、省去“返回主菜单”按钮
对于手持式计算设备(手机、Pad等)而言,设备本身带有一个返回按钮,因此,应用中的“返回主菜单”按钮有些画蛇添足之嫌。