7.4 CD唱片应用程序
在学习了环境和数据管理之后,现在是时候改进这个应用程序了。dbm数据库看起来很适合于存储CD资料,所以下面将使用dbm来实现数据存储。
7.4.1 更新设计
因为这次的更新会涉及大量代码的重写,所以现在是个重新审视设计决策以查看哪些地方需要改进的好时机。虽然在文件中以逗号分隔变量来存储信息是一种在shell中很容易实现的方式,但这样做的局限性也很大,因为许多CD标题和曲目都包含逗号。你可以通过使用dbm数据库来完全放弃这种分隔方法,这也是我们需要改变的一个设计元素。
将CD资料分为标题和曲目两个部分,并用不同的文件来分别保存它们。
前面的实现多少都存在着这样一个问题,即将应用程序的数据访问部分和用户接口部分混在了一起,这与程序全实现在一个文件中有很大的关系。在这个新的实现中,你将用一个头文件来描述数据和用于访问它的例程,并将用户接口代码和数据处理代码分别放到两个文件中去。
虽然可以继续用curses来实现用户接口,但本次实现将返回到简单的基于行的系统。这不仅使应用程序的用户接口部分既短小又简单,而且可以把精力集中到其他实现方面上去。
虽然还不能在dbm代码中使用SQL语句,但可以使用SQL术语以更正规的方式来描述新数据库。如果还不熟悉SQL语句,不用担心,我们会解释这些定义。你还将在第8章中看到更多对SQL语句的介绍。表可以用下面的代码来描述:
这个非常简洁的描述表明数据域的名字和长度。cdc_entry表中每个记录都有一个唯一的catalog列。cdt_entry表中曲目号不能为零,而且catalog和track_no两列的组合是唯一的。你将在下一节的代码中看到这些描述被定义为typedef struct结构。
7.4.2 使用dbm数据库的CD唱片应用程序
你现在将通过使用dbm数据库存储信息的方法来重新实现应用程序。整个应用程序共有3个文件,它们是cd_data.h、app_ui.c和cd_access.c。
你还将把用户接口重写为命令行程序。在本书的后面章节中,你将看到使用不同的客户/服务器机制来实现应用程序,并最终将其实现为一个能够通过Web浏览器跨网络访问的应用程序,到那时,你还将重用这里的数据库接口和一部分的用户接口。把接口转换为更简单的命令行驱动接口,使你能更容易关注应用程序最重要的部分,而不是用户接口。
你将在后面的章节中看到,数据库的头文件cd_data.h和来自文件cd_access.c里的函数被多次重用。
请记住,有些Linux发行版需要稍微不同的编译选项,如在C源文件中包含头文件gdbm-ndbm.h而不是ndbm.h,使用-lgdbm_compat -lgdhm而不是只使用-lgdbm。如果你的Linux发行版就属于这种情况,就需要对文件access.c和Makefile进行适当的修改。
实 验 cd_data.h
我们从头文件开始,它定义了数据的结构和用于访问这些数据的例程。
(1)下面是CD数据库的数据结构的定义。它定义了组成数据库的两个表的结构和大小。首先定义了几个将会用到的数据域长度以及两个结构:一个用于标题数据项,另一个用于曲目数据项:
(2)在定义了一些数据结构后,你可以开始定义一些需要的访问例程了。函数名中包含cdc的函数负责处理标题数据项,包含cdt的函数负责处理曲目数据项:
注意,有些函数直接返回数据结构。你可以通过强制设置这些结构的内容为空,来表明函数调用失败。
实 验 app_ui.c
现在开始介绍用户接口。这部分程序相对来说比较简单,它实现在一个单独的文件中,你将用它来访问数据库函数。
(1)同往常一样,从头文件开始:
(2)用typedef语句定义菜单选项。这要比用#define语句定义常量的方法好,因为它允许编译器检查菜单选项变量的类型:
(3)现在开始编写各种局部函数的原型。记住,实际访问数据库的函数的原型是通过头文件cd_data.h包含进来的:
(4)最后,到了main函数。它先对current_cdc_entry结构进行初始化,用它来保存当前选中的CD标题项。还解析了命令行,宣布正在运行的是哪个程序,并初始化数据库:
(5)现在已准备好处理用户输入了。进入一个循环,等待用户选择一个菜单选项,然后处理它,直到用户选择退出选项为止。注意,把current_cdc_entry结构传递给show_menu函数。这是为了让菜单选项能够根据用户当前选择的标题项做相应的改变:
(6)主循环退出时,关闭数据库并退回到环境。announce函数用于输出欢迎辞:
(7)下面列出了show_menu函数的内容。这个函数通过标题名的第一个字符来检查当前标题项是否被选中。如果选择了一个标题项,用户将看到更多的菜单选项:
注意,现在要用数字来选择菜单项,而不像在前两个例子中那样使用首字母。
(8)你需要在多个地方询问用户,让用户确认他的请求。我们并未让这段提问代码多次出现在程序中,而是抽取这段代码组成一个单独的函数get_confirm:
(9)函数enter_new_cat_entry的作用是让用户输入一个新的标题项。你并不想保存由fgets函数返回的换行符,所以把它去掉:
注意,你没有使用gets函数,因为它无法检查缓存区是否溢出。你应总是避免使用gets函数!
(10)下面是用于输入曲目信息的函数enter_new_track_entries。这个函数比标题项函数要稍微复杂一点,因为你允许保留已经存在的曲目项:
(11)首先,必须检查当前曲目编号处是否已有曲目存在。根据查询结果,程序将对提示做相应的修改:
(12)如果当前曲目编号处没有现存曲目,而且用户也未添加一条记录,则程序就认为曲目都已经添加完毕了:
(13)如果用户输入一个单独的字符d,这将会删除当前以及更高编号的曲目记录。如果del_cdt_entry函数找不到待删除的曲目,它将会返回false:
(14)下面这段代码的作用是添加一个新的曲目或者更新一个现有曲目。首先构建一个cdt_entry结构new_track,然后调用数据库函数add_cdt_entry来把它添加到数据库中:
(15)函数del_cat_entry删除一个标题项。如果标题项被删除了,那么原来属于它的曲目记录也都将被删除:
(16)接下来这个函数的作用是删除与某个标题项对应的所有曲目:
(17)下面是一个非常简单的标题搜索函数。它允许用户输入一个字符串,然后查找包含这个字符串的标题项。因为可能存在多个匹配的记录,所以只是依次将每个匹配的记录提供给用户:
(18)list_tracks函数用于输出指定标题项的所有曲目:
(19)count_all_entries函数用于统计所有曲目数量:
(20)下面是display_cdc函数,它用来显示一条标题项记录:
display_cdt函数的作用是显示一条曲目项记录:
(21)strip_return函数的作用是删除字符串尾部的换行符。记住,Linux同UNIX一样,使用一个单独的换行符来表明一行的结束:
(22)command_mode是一个对命令行参数进行解析的函数。其中调用的getopt函数是一个确保程序能够接受符合标准Linux规范的参数的好方法:
实 验 cd_access.c
现在开始介绍用于访问dbm数据库的函数:
(1)与往常一样,你从包含头文件开始。然后用#define语句指定将用来存储数据的文件:
(2)使用下面两个文件范围变量追踪当前的数据库:
(3)默认情况下,database_initialize函数打开一个已有的数据库,但通过传递一个非零的(即布尔值为真)参数new_database给它,你就可以强迫它创建一个新的(空)数据库,并有效地删除任何已有的数据库。如果数据库被成功初始化,那么两个数据库指针也被初始化,以此表明数据库被打开:
(4)database_close函数用于关闭已打开的数据库,并将两个数据库指针设为null,以此表明当前没有打开的数据库:
(5)接下来这个函数,当给它传递一个指向标题项文本字符串的指针时,它将检索出一个标题项来。如果标题项没有找到,其返回数据中的标题域将为空:
(6)函数先做一些完整性检查,确保数据库已打开而且你传递了一个合理的参数,即搜索关键字里只包含有效的字符串和null:
(7)设置dbm函数需要的datum结构,然后使用dbm_fetch函数来检索数据。如果没有数据可以获得,你将返回先前初始化过的空的entry_to_return结构:
(8)你希望还能对一个单独的曲目项进行检索,这正是下面这个函数实现的功能。它与get_cdc_entry函数的工作方式基本类似,不过它需要一个指向标题字符串的指针和一个曲目编号作为参数:
(9)下一个函数add_cdc_entry的作用是增加一个新的标题项记录:
(10)add_cdt_entry函数的作用是增加一个新的曲目项记录。标题字符串和曲目编号组合在一起构成其访问关键字:
(11)既然可以往数据库里增加数据,你最好还能删除它们。下面这个函数的作用就是删除标题项记录:
(12)与上面的函数类似,这个函数用于删除曲目记录。记住,曲目关键字是由标题项字符串和曲目编号两者构成的一个复合索引:
(13)最后非常重要的一点是,你还有一个简单的搜索函数。它不是非常复杂,但它演示了如何在预先不知道关键字的情况下扫描全部的dbm记录项。
因为你事先并不知道会有多少匹配的记录项,所以你将这个函数实现为每次调用返回一个记录项。如果什么也没找到,记录项就将是空的。为了扫描整个数据库,你在调用这个函数时使用一个指向整数的指针*first_call_prt,它在函数第一次被调用时应被设置为1,然后这个函数就知道它应该在数据库的起始处开始搜索。在后续的调用中,这个变量将被设置为0,函数将会从上次找到记录项的位置开始继续搜索。
当希望重新开始搜索时,比如要搜索另外一个标题项时,你必须把*first_call_ptr的值设为真,然后再次调用这个函数,这将重新初始化搜索。
在这个函数的两次调用之间,函数维护一些内部状态信息。这样做的目的是向客户隐藏继续搜索的复杂性,同时保留了搜索函数在具体实现方面的秘密。
如果搜索文本指针指向null字符,那么所有的记录项都将被认为是匹配的。
(14)和往常一样,先做完整性检查:
(15)如果这个函数被调用时,first_call_ptr被设置为true,就表示你需要从数据库的起始位置开始搜索(或重新开始搜索)。如果first_call_ptr的值不是true,你只需移动到数据库中的下一个关键字:
(16)搜索方式非常简单,它只是检查当前标题项是否包含搜索字符串:
现在你将通过下面的makefile文件把所有的程序结合在一起。现在还无须太过操心它,因为你马上就要在下一章中了解它的工作原理。目前你只需敲入它的内容并将其保存为Makefile文件即可:
要想编译这个新的CD唱片应用程序,你需要在提示符后输入下面的命令:
$ make
如果一切顺利,可执行文件application将被编译并放置到当前目录中。