第26章:自上而下的设计
当程序变得更大且更复杂的时候,它们也变得更难于设计、编码和维护。对于任何大型项目,始终奏效的一个办法是,将大型复杂的任务分解为一系列小型简单的任务。来想象一下我们正尝试对一个从火星过来的人描述一件日常常见的任务,去市场买一下食物。我们可以将全部进程描述为下列的一系列步骤:
坐上车。
开车去市场。
停车。
进入市场。
采购食物。
回到车中。
开车回家。
停车。
进入屋子。
然而,对于一个从火星来的人可能需要更多细节。我们可以进一步的分解子任务「停车」为下列一系列步骤:
找到停车位。
将车子开进停车位。
关闭马达。
设置驻车制动。
下车。
锁车。
「关闭马达」子任务可以更进一步分解为包含「关闭点火器」、「取出点火钥匙」等等,知道去市场的整个进程中的每个步骤都得到完整的定义为止。
确定顶层步骤,并开发越来越详细的步骤视图,称之为自上而下的设计(top-down design)。该技术允许我们分解大型复杂任务为众多小型简单的任务。自上而下的设计是设计编程的常见方法,其中也特别适合 shell 编程。
本章中,我们将使用自上而下的设计以更进一步开发报表生成脚本。
Shell 函数
我们的脚本执行下列步骤以生成 HTML 文档:
打开页面。
打开页面头部信息。
设置页面标题。
关闭页面头部信息。
打开页面主体。
输出页面标题。
输出时间戳。
关闭页面主体。
关闭页面。
对于下一步的开发,我们会在第 7 步和第 8 步之间加入一些任务。包括下面这些:
系统正常运行时间和负载。这是从上次关机或重启开始的时长,和在几个时间间隔内当前运行在处理器上的平均任务数目。
磁盘空间。这是系统存储设备的空间总体情况。
主目录空间。这是每个用户的存储空间的用量。
如果对于每个任务都有一个命令的话,我们可以简单地用命令替换将它们加入到脚本中。
我们可以用两种方法创建额外的命令。我们可以写三个独立的脚本,将其放在 PATH 变量中的目录中,或者可以将程序作为 shell 函数(shell functions)内嵌在脚本中。我们已经提到过,shell 函数是位于其它脚本中的「迷你脚本」,其行为如同独立的程序。shell 函数有两种句法形式。第一个是更为正规的形式:
还有一个更简单(通常也是首选)的形式:
name
是函数的名称,而 commands
是函数中一系列的命令。两种形式是等效的,可互换使用。下面是一个脚本,演示了如何使用 shell 函数:
当 shell 读取脚本时,它略过第 1 到 11 行,因为这些行是由注释和函数定义组成的。从第 12 行的 echo
命令开始执行。第 13 行呼叫(call)了 shell 函数 step2
,然后 shell 如同执行其它命令一样的执行了函数。程序控制随后移动到了第 6 行,执行了第二个 echo
命令。接下来是第 7 行。return
命令结束函数,将控制返回给呼叫函数的下一行(第 14 行),最后一个 echo
命令得以执行。注意,对于函数呼叫,shell 函数能被识别,对于外部程序而言,其名称则不会得到解释。shell 函数的定义在脚本中必须出现在它们被呼叫之前。
来加一个最小的 shell 函数定义到我们的脚本中,如下:
shell 函数的命名遵循变量的命名规则。一个函数必须包含至少一条命令。return
命令(可选的)满足这个需求。
局部变量
迄今为止我们所写的脚本中,所有的变量(包括常量)都是全局变量(global variables)。全局变量在整个程序中保持其存在。这在很多情况中是好的,但是有时会使得 shell 函数用起来更复杂。在 shell 函数中,经常希望使用局部变量(local variables)。局部变量仅可在定义它们的 shell 函数中访问,一旦 shell 函数终止则不复存在。
使用局部变量允许程序员使用可能已经存在的变量名,无论该名称是在全局的脚本中,还是在其它 shell 函数中,不用担心潜在的命名冲突。
下面是一个示例脚本,演示了如何定义和使用局部变量:
可以看到,局部变量的定义,是在变量名之前加上 local
这个单词。这样就创建了一个局限于定义它的 shell 函数中的变量。一旦跳出 shell 函数,变量就不复存在了。当我们运行这个脚本,会看到这样的结果:
我们看到对两个 shell 函数中的局部变量 foo
的赋值,对函数外的 foo
变量的定义没有影响。
这一功能允许 shell 函数在编写时独立于所在的脚本,同时保持彼此间的独立性。这是有价值的,因为它有助于防止程序的一部分干扰其它部分。它还允许编写的 shell 函数可移植。亦即可以按需在脚本间剪切粘贴。
保持脚本运行
在开发程序的过程中,将程序保持在一个可运行的状态中会有所助益。保持其运行状态,且频繁测试,可以在开发进程的早期检测出错误。会使得调试程序变得更容易。例如,如果我们运行程序,作出一个微小的改动,然后再次运行程序并发现了一个问题,看起来最近所作的修改很可能就是问题的根源。通过添加空函数,在程序员中称为存根(stubs),我们可以在早期验证程序的逻辑流程。构造存根时,最好包含一些可以向程序员提供反馈的东西,表明逻辑流程正在执行。如果我们现在查看一下脚本的输出:
我们看到在时间戳之后,有几行空白输出,但是不能确定原因。如果我们将更改函数,加入一些反馈:
随后再次运行脚本:
现在可以看到,实际上执行了三个函数。
鉴于我们的函数框架已经可以工作,是时候充实一下我们的函数代码了。首先来看 report_uptime
函数:
相当简单明了。我们使用了一个 here 文档以输出一个节标题和 uptime
命令的输出,用 <pre
标签以保存命令的格式。report_disk_space
函数类似。
函数使用了 df -h
命令以判断磁盘空间的使用量。最后,我们来构建 report_home_space
函数。
我们使用带 -sh
的 du
命令以执行该任务。然而这并不是问题的完整的解决方案。它可以在一些系统上工作(例如 Ubuntu),但是在其它系统上却工作不正常。原因在于许多系统设置了家目录的权限许可,以防止人人都可读,这是一个合理的安全措施。在这些系统中,上面这个 report_home_space
函数仅会在以超级用户权限运行时才会正常工作。一个更佳的解决方案是根据用户的权限调整脚本的行为。我们将在下章中介绍。
在
.bashrc
文件中的 shell 函数shell 函数制作优秀的别名替代品,并且实际上是创建供个人用的小命令的优选方式。别名受限于命令的种类和它们支持的 shell 特性,而 shell 函数则允许任意编写脚本中的命令。例如,如果我们喜欢在脚本中的
report_disk_space
shell 函数,我们可以在.bashrc
文件中创建一个类似的名为ds
的函数:
总结
本章中,我们介绍了一种常见的程序开发方法,名为自上而下的设计,学习了如何使用 shell 函数来构建逐步所需的优化。还学习了可以用局部变量使 shell 函数保持在脚本中的独立和彼此间的独立。这使得编写可移植的 shell 函数成为可能,并且可以放置在多个程序中复用(reusable),非常节约时间。
扩展阅读
关于软件设计哲学,维基百科有很多条目。这里有一些上佳的文章:
Last updated
Was this helpful?