 | 级别: 初级 Teodor Zlatanov (tzz@iglou.com), 程序员, Gold Software Systems
2003 年 7 月 09 日 如果使用手工构建方法,那么基于文件的配置很快就会崩溃。在本文中,Ted 演示了 AppConfig 模块如何处理本地配置存储。他研究了 AppConfig 模块的高级特性:验证、自动操作、修改散列和数组变量以及“单选按钮”样式的互斥选项。在本文中,我将讨论如何通过使用 CPAN AppConfig 模块来为 Perl 程序进行高级的、基于文件的配置。有关AppConfig 的介绍以及使用配置管理模块背后的基本原理,请参阅我上一篇有关这一主题的“功能丰富的 Perl”文章
本文(第二篇文章)中的一些代码利用了第一篇文章中的代码,但没有详细说明。
为了理解我将要介绍的内容,您需要阅读第一篇文章。
在开始之前,系统上应该有 Perl 5.005 或更高版本,
而且在系统上安装了 CPAN AppConfig 模块(如果需要,请参阅本文后面的
参考资料以获得这些软件包的链接)。
验证
数据验证在匆忙的开发过程中常常被遗忘,它是您程序现在必须接种的疫苗,这样您的程序以后才不会崩溃。
用户期待程序处理他们的输入,而您应该预期会出现意外情况。
幸运的是,您可以使用 AppConfig 数据验证例程。
根据初始化 AppConfig 时给定的 PEDANTIC 设置,
可以拒绝错误数据,或者在发现恶意数据时异常终止整个程序。
清单 1. 验证配置值
# SSN: must be a string with exactly 9 digits
# LIMITED: must be a string key existing in the hash %limited_hash
$config->define(
# more complex subroutine validation
'SSN' => { ARGCOUNT => ARGCOUNT_ONE,
VALIDATE => sub
{
my $varname = shift @_;
my $value = shift @_;
# only succeed with 9 digits in $value
return (9 == ($value =~ tr/0-9//));
}
},
# limited values, must be 'alpha' or 'beta'
'LIMITED' => { ARGCOUNT => ARGCOUNT_ONE,
VALIDATE => sub
{
my $varname = shift @_;
my $value = shift @_;
my %limited_hash = ( alpha => 1, beta => 2 );
return exists $limited_hash{$value};
}
},
);
|
注:VALIDATE 子例程要比 ACTION 子例程(将在下一节中描述)受到更多的限制。AppConfig::State 实例没有被传递到 VALIDATE 子例程,所以我们不能容易地访问配置中的其它变量。
或许是故意这样安排的,这样,喜欢冒险的程序员就不会将 AppConfig::State 置于不确定状态。
自动操作
AppConfig 模块使得在设置变量值时触发函数变得容易。ACTION 子例程在设置值
之后被调用,
而 VALIDATE 子例程在设置值之前被调用。
不应该使用 ACTION 来设置当前变量的值。
如果不注意的话,会启动无限循环。例如,试试下面的示例:
清单 2. 无限自动操作循环 — 不要使用!
$config->define(
'TRIGGER' => { ARGCOUNT => ARGCOUNT_ONE,
ACTION => sub # autoaction
{
my $config = shift @_;
my $varname = shift @_;
my $value = shift @_;
print "$varname = $value\n";
$value++;
$config->TRIGGER($value);
}
}
);
|
清单 2 中的代码将一直运行下去。可以利用外部变量采取安全措施,但这会很快引起混乱。
取而代之的方法是,在读入所有数据之后再进行数据的后处理。
我们将在以后的文章中进一步研究自动操作,
那时我们将讨论变量值如何关闭其它变量的触发器。在那里,您将看到如何从变量自身的触发器内部安全地为互斥变量设置变量值。
修改 AppConfig 散列和数组变量
如何修改 AppConfig 对象中的标量值很明了:只要调用
$config->VARIABLE(value) 。
然而,数组和散列更为复杂。当您请求变量值时,AppConfig 对象将给您数组或散列引用。
您必须修改对象中的数据,而不是传递给您的数据。
不能除去单个元素;必须除去所有元素,然后重新插入需要的元素。
可以如下修改数组:
清单 3. 修改数组
$config->define('HOSTS' => { ARGCOUNT => ARGCOUNT_LIST });
# add a value to the array named HOSTS
$config->HOSTS("jove");
# list the values in the HOSTS array
print "$_\n" foreach @{$config->HOSTS};
# reset the array to 0 entries
$config->_default("HOSTS");
|
没有用于直接访问数组值的 API。可以通过访问内部的 AppConfig::State 数据或
作为数组变量值返回给您的数组引用来实现这一步,但我们建议您不要这样做。
散列项类似于数组,易于设置但难以除去。
另外,它们的值被限制为标量字符串或 undef。
清单 4. 修改散列
$config->define('PHONE' => { ARGCOUNT => ARGCOUNT_HASH });
# add values to the hash named PHONE
$config->PHONE("jimmy=1-800-453-2211");
# unusual cases
$config->PHONE("equals=a=b"); # the value will be "a=b"
$config->PHONE("harry="); # the value will be a string of length 0 ('')
$config->PHONE("harry"); # the value will be undef
# list the sorted keys and their values in the PHONE hash
print "$_ => ", $config->PHONE()->{$_}, "\n" foreach sort keys %{$config->PHONE};
# you can also do it with Data::Dumper
print Dumper $config->PHONE;
# reset the hash to 0 entries
$config->_default("PHONE");
|
就象数组那样,没有用于直接访问散列值的 API。
可以通过访问内部的 AppConfig::State 数据或作为散列变量值返回给您的散列引用来实现这一步,但我们建议您不要这样做。
单选按钮
所有 GUI 用户都熟悉单选按钮:定义一系列互斥选项。
例如,文件类型可以是“可执行文件”、“目录”或“特殊”(任何别的类型)。
可以使用数字类型来处理它;例如,可执行文件为 1,目录为 2,特殊为 3。
但在单选按钮中,当设置了三个选项中的任何一个时,其它两个就被自动设置为 0。
实现依赖于选项的二进制状态(开或关)。
当选项为关时,不需要另外做任何事情。当选项设置为开时,其它两个选项将设置为 0。
将所有这三个选项设置为 0 是可行的;可以将它视为一个未知状态(于是有四种可能的状态:文件、目录、特殊和未知)。
清单 5. 单选按钮
my $config = AppConfig->new();
$config->define(
# mutually exclusive options
'FILE' => { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ACTION => \&toggle_type },
'DIRECTORY' => { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ACTION => \&toggle_type },
'SPECIAL' => { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 1, ACTION => \&toggle_type },
);
# {{{ toggle_type: do the right thing when file type is specified (3 mutually exclusive options)
sub toggle_type
{
my $self = shift @_;
my $varname = shift @_;
my $value = shift @_;
return unless $value; # do nothing if we're being turned off
# turn off all options except the one that invoked this action
$self->set($_, 0) foreach grep !/^$varname$/i, qw/FILE DIRECTORY SPECIAL/;
}
|
上面代码的关键是与所有其它 AppConfig 一起使用,不需要新模块。
它能使代码正常运行。
如果访问了 AppConfig::State 的内部,其实现就会不同(所以,例如,我们不必调用
set() ),但对于我们这三个互斥设置的示例,这不是必需的。
相互依赖的选项
下面是当选项更为复杂时将修改 AppConfig::State 内部的代码。
重要的是如何在 AppConfig::State 内部直接修改变量值 — 我们不打算按正常方式做的一些事情。
当 TIMES 散列有多个项时,该示例将自动设置 WINNER 和 LOSER 变量。
您可以运行这个完整的示例。
另请注意变量间相互作用的复杂程度。什么时候设置 WINNER 和 LOSER 是安全的呢?
当直接设置 WINNER 和 LOSER 时,它们应该使用
TIMES 散列,还是应该直接在 AppConfig::State 内部修改值?这针对的只是三个变量。
想象一下对于希望使用相互依赖的选项的小程序而言,这会变得多么复杂!
如果您有许多触发其它选项或操作的选项,则应该考虑使用有限状态机。FSM 最适合这种复杂环境。
清单 6. 相互依赖的选项
#!/usr/bin/perl -w
use strict;
use AppConfig qw/:argcount/;
my $config = AppConfig->new();
$config->define(
'TIMES' => { ARGCOUNT => ARGCOUNT_HASH, ACTION => \&toggle_option },
'WINNER' => { ARGCOUNT => ARGCOUNT_ONE, ACTION => \&cheat_action },
'LOSER' => { ARGCOUNT => ARGCOUNT_ONE, ACTION => \&cheat_action },
'CHEATERS' => { ARGCOUNT => ARGCOUNT_NONE },
);
$config->TIMES("Johnny Boy = .444");
$config->TIMES("Johnny Appleseed = 2.45");
$config->TIMES("Johnny Be Good = 8.002");
print "Winner: ", $config->WINNER, "\n";
print "Loser: ", $config->LOSER, "\n";
$config->WINNER("Johnny Be Good");
print "Winner: ", $config->WINNER, "\n";
print "Loser: ", $config->LOSER, "\n";
$config->LOSER("Johnny Be Good");
print "Winner: ", $config->WINNER, "\n";
print "Loser: ", $config->LOSER, "\n";
# {{{ toggle_option: toggle interdependent options
sub toggle_option
{
my $self = shift @_;
my $varname = shift @_;
my $value = shift @_;
my @sorted = sort { $self->TIMES()->{$a} <=> $self->TIMES()->{$b} } keys %{$self->TIMES};
if (scalar @sorted > 1) # we need more than 1 member to have a winner and a loser
{
# we have to set winner and loser directly to avoid the cheat_action
$self->{VARIABLE}->{winner} = $sorted[0];
$self->{VARIABLE}->{loser} = $sorted[-1];
}
}
# }}}
# {{{ cheat_action: set TIMES hash value for a given key so it's always the winner or the loser
sub cheat_action
{
my $self = shift @_;
my $varname = shift @_;
my $value = shift @_;
my $time;
$varname = uc $varname; # make sure we match the variable name
if ($varname eq 'WINNER')
{
# set the time to 0 unconditionally
$time = 0;
}
elsif ($varname eq 'LOSER')
{
# set the time to the sum of all the times (it will always be the worst time, then)
$time += $_ foreach values %{$config->TIMES()};
}
# trigger a resorting by inserting the newly wanted time
$self->TIMES("$value=$time");
}
# }}}
|

 |

|
进一步使用它
这里显示的高级技术应该有助于您改进自己程序的配置。
不要担心自己动手对 AppConfig 进行扩展!
参考资料
关于作者  | 
|  | Teodor Zlatanov 于 1999 年从美国波士顿大学(Boston University)毕业,获得计算机工程硕士学位。他从 1992 年起就从事程序员的工作,使用了 Perl、Java、C 和 C++。他的兴趣是文本解析、三层客户机-服务器数据库体系结构、UNIX 系统管理、CORBA 和项目管理方面的开放源码工作。可以通过
tzz@iglou.com与 Teodor 联系。
|
对本文的评价
|  |