构建优秀的CPAN模块
当你计划将模块发布到CPAN时,你的首要任务之一是确定你将支持和不支持的操作系统、Perl版本和其他环境。通常,这些答案将基于你想要提供的功能和所使用的模块和库,确定你能和不能支持什么。
然而,许多CPAN模块无意中限制了它们可以工作的地点。你可以采取几个步骤来消除这些限制。通常,这些步骤是简单的更改,实际上可以增强你的模块的功能性和可维护性。
在我的机器上运行
你拥有最新的PowerBook,每天从CPAN更新,并运行最新的Perl版本。使用你的模块的人并不这样。记住,仅仅因为一个应用程序或操作系统比你的祖母还老,并不意味着它已经不再有用。代码不会随着时间的推移自发地产生错误,也不会积累使它运行得更慢的垃圾。一些至关重要的应用程序在30年以上没有改动过,而这些语言在你还是婴儿的时候就已经被废弃了。这些应用程序在例如,让灯光亮起并追踪世界上所有的钱,它们通常在非常旧的计算机上运行。
公司想要继续使用它们的老系统,因为这些系统是有效的,并且它们想要使用Perl,因为Perl可以在任何地方运行。如果你可以利用CPAN,你已经有了一个Perl应用程序的90%。
入乡随俗
Perl在至少93种不同的操作系统上运行。此外,还有18种不同的生产化Perl 5版本在流传(不包括开发分支和构建选项)。93 x 18 = 1674
。这意味着你的模块可以在超过1500个不同的操作系统/Perl版本环境中运行。加上线程、Unicode和其他选项,你根本无法测试你的糟糕模块将在所有这些地方结束运行!
幸运的是,Perl也提供了(许多)答案。
定义你的需求
如果你知道你的模块在某些环境中根本无法运行,你应该设置先决条件。这允许你为用户提供一层安全保障。先决条件包括
你的模块无法运行的操作系统
检查
$^O
和%Config
来确定这一点。$^O
会告诉你操作系统的名称。有时这不够具体,所以你可以检查%Config
。use Config; if ( $Config{ osname } ne 'solaris' || $Config{ osver } < 2.9 ) { die "This module needs Solaris 2.9 or higher to run.\n"; }
通常最好限制自己在已知性能良好的特定操作系统集合中。随着你模块的流行,用户会告诉你它是否在其他地方工作。
Perl版本/功能
检查
$]
和%INC
来确定这一点。$]
包含Perl版本,而%INC
包含迄今为止加载的Perl模块列表。(参见线程部分的示例。)如果你的模块在某个特定版本的Perl之前根本无法运行,请确保你在模块中有一个use 5.00#
(其中#
是你需要的版本)。此外,Module::Build 允许你在构造函数的requires
选项中指定最小Perl版本。模块/库
在 ExtUtils::MakeMaker 中,你可以在对
WriteMakefile()
的调用中指定PREREQ_PM
,以表明你的模块需要其他模块才能运行。这可以包括版本号,既包括最小版本,也包括最大可接受版本。Module::Build
具有类似的特性,具有构造函数的requires
选项。如果你依赖于外部的、非Perl库,在继续之前你应该查看它们是否存在。像所有其他事情一样,CPAN也有解决方案:App::Info。
use App::Info::HTTPD::Apache; my $app = App::Info::HTTPD::Apache->new; unless ( $app->installed ) { die "Apache isn't installed!\n"; }
操作系统
你的模块恰好运行在什么操作系统上,这个问题比大多数人意识到的要重要,也可能不那么重要。我们中的大多数人都在Unix-land和Windows-land工作过,所以我们对目录分隔符和外部可执行文件的硬编码很了解。然而,还有其他问题只在你的模块运行在VMS这样的地方时才会出现。
例如,VMS文件系统有在完全限定文件名中包含卷的概念。VMS在处理文件权限和文件版本化方面也与标准的Unix/Win32/Mac模型有很大不同。如何处理这些差异的一个很好的例子是核心模块File::Spec。
因为这是大多数作者在某个时候都必须面对的问题,所以有一个叫做perlpod
的标准,恰如其分地叫做perlport
。如果你遵循那里的内容,你将会没事。
Perl版本
Perl 5.0.0发布已经超过十年了,在这段时间里,Perl发生了很大变化。然而,大多数安装并不是最新的和最好的版本。主要原因是因为“如果它没坏,就不去修。”没有所谓的安全的升级。
大多数应用程序不需要最新的功能,也不会遇到大多数的bug或安全漏洞。它们并不那么复杂。如果你将你的模块限制在仅在5.8或甚至5.6中存在的功能,你将忽略大量的潜在用户。
安全改进
大多数安全修复对程序员来说是透明的。如果Perl散列背后的算法得到改进,你不会看到。如果新版本修复了suidperl
中的一个漏洞,你的模块不会关心。
然而,有时安全修复是一个新特性,其使用将(并且应该)成为接受的标准:例如,5.6中的open()
的三参数形式。在这些情况下,我使用string-eval
来尝试使用新特性,如果不行则回退到旧特性。(在这里检查$]
没有帮助,因为如果你的Perl版本是5.6之前的,它仍然会尝试编译三参数形式并抱怨。)
eval q{
open( INFILE, ">", $filename )
or die "Cannot open '$filename' for writing: $!\n";
}; if ( $@ ) {
# Check to see if it's a compile error
if ( $@ =~ /Too many arguments for open/ ) {
open( INFILE, "> $filename" )
or die "Cannot open '$filename' for writing: $!\n";
}
else {
# Otherwise, rethrow the error
die $@;
}
}
bug修复
与安全修复一样,大多数bug修复对程序员来说是透明的。我们中的大多数人都没有注意到5.8.0中的散列算法不是最优的,并且在5.8.1中有几个改进。我知道我没有注意到。总的来说,这些不会影响你。
与安全修复不同,如果你的模块在Perl先前版本中的一个bug上崩溃,你可能无法做太多,除了要求那个包含bug修复的版本。
新特性
每个人都听说过5.6.0中出现的use warnings;
和our
。你可能不知道的是一些较小的变化。一个很好的例子是排序。
5.8.0将排序改为稳定的。这意味着如果两个项目比较相等,则结果列表将保留它们的原始顺序。Perl的先前版本没有这样的保证。这意味着像这样的代码可能不会按预期工作
my @input = qw( abcd abce efgh );
my @output = sort {
substr( $a, 0, 3 ) cmp substr( $b, 0, 3 )
} @input;
如果你依赖于@output
将包含qw( abcd abce efgh )
的事实,你的模块可能在前5.8.0版本的版本上遇到问题。@output
可能包含qw( abce abcd efgh)
,因为排序函数认为abcd
和abce
是相同的。
与操作系统和Perl版本相关的陷阱
你的模块在操作系统或Perl版本上可能很纯净。你的整个分发版呢?你的测试可能暴露了你没有意识到的依赖。
例如,5.6.0版本增加了词法作用域的警告。不再需要将-w
标志传递给Perl可执行文件,现在可以说use warnings
。因为启用警告通常是一件好事,这对于使用Perl 5.6.0+的认真程序员编写的测试文件来说是一个非常常见的头信息。
use strict;
use warnings;
use Test::More tests => 42;
现在,即使你的模块在5.6.0之前的Perl版本上运行,你的测试也不会!这意味着你的分发不会通过CPAN或CPANPLUS安装。对于通过这种方式安装模块并且有比调试模块测试更好的事情要做的管理员来说,他们不会安装它。
主要新特性
一些新特性非常大,以至于改变了游戏规则。这些包括Unicode和多线程。Unicode在各种形式的Perl 5版本中都有支持。这种支持已经从模块(如Unicode::String)逐渐移动到Perl核心本身。
多线程
在5.8.0中,Perl的线程模型从5.005模型(从来就没有很好地工作)变更为ithreads(确实可以工作)。此外,多核处理器正在进入小型服务器。越来越多的开发者选择使用5.8+编写线程化应用程序。
这意味着你的模块可能需要在线程化的游乐场中运行,这对面向过程的程序员来说确实是个奇怪的地方。现在,Perl的线程模型默认是不共享的,这意味着全局变量可以避免相互覆盖。这与默认共享所有变量的标准线程模型(如Java的)不同。因为这个决定,大多数模块在线程下运行时几乎不需要任何改变。
你需要解决的主要问题是你的有状态变量会发生什么。这些变量在子例程调用之间持续存在并保持值,但需要在线程之间进行协调。一个很好的例子是
{
my $counter;
sub next_value ( return ++$counter; }
}
如果你依赖于这个计数器在每次调用next_value()
子例程时进行协调,你需要采取三个步骤。
共享
因为Perl不会为你共享变量,你必须显式地共享
$counter
以确保它能够在线程之间正确更新。锁定
因为线程之间的上下文切换可以在任何时候发生,你需要在
next_value()
子例程中锁定$counter
。版本兼容性
此外,因为ithreads是5.8.0+的可选特性,而
lock()
子例程在5.6.0+之前是未定义的,你可能需要进行一些版本检查。{ my $counter = 0; if ( $] >= 5.008 && exists $INC{'threads.pm'} ) { require threads::shared; import threads::shared qw(share); share( $counter ); } else { *lock = sub (*) {} } sub next_value { lock( $counter ); $counter++; } }
关于如何成功地将应用程序移植到线程化工作的最好描述,我见过的是“Where Wizards Fear to Tread”关于Perl 5.8线程。
Unicode
尽管在5.8.0之前Unicode有一些支持,但在5.8.0的一个主要特性是在Perl本身中几乎无缝地处理Unicode。在此之前,开发者必须使用Unicode::String和其他模块。这意味着如果你认为支持5.8.0之前的Perl的Unicode很重要,你应该尽可能小心地处理字符串。幸运的是,大多数主要模块已经为你做了这件事,你不必担心它。
讨论如何干净地处理Unicode本身是一篇文章的内容。请参阅perlunicode
和perluniintro
以获取更多信息。
与其他程序友好地协作
如果你像我一样,你在幼儿园的时候经常听到“不与其他人友好”。虽然这是一个值得称赞的特质,但对于生产系统所依赖的任何模块来说,这不是值得赞扬的。当试图与其他程序友好地协作时,有几个常见的项目需要留意。
持久的环境
持久环境,如 mod_perl
和 FastCGI,是生活的事实。它们使万维网得以运行。它们与基本脚本在运行、执行任务、结束时的行为非常不同。基本上,持久环境,如 mod_perl
,执行以下几件事。
持久解释器
启动 Perl 可执行文件的成本相对较高。在一个像网络应用程序这样的环境中,每个请求都是一个独立的 Perl 脚本调用。持久性在调用之间在内存中保持 Perl 解释器,从而大大减少了启动开销。
已分叉的子进程
为了同时处理多个请求,持久环境通常提供已分叉的子进程的能力,每个子进程都有自己的解释器。通常,这需要在每个子进程的内存区域中复制每个模块。
共享内存
几乎每个请求都会使用相同的模块(CGI、DBI 等)。而不是每次都加载它们,持久环境将它们加载到共享内存中,每个子进程都可以访问。这可以节省大量的内存,否则将需要为每个子进程加载 DBI 一次。这允许同一台机器创建更多的子进程来同时处理同一台机器上的更多请求。
缓存需要特别提及。由于大多数持久环境在分叉子进程之前将大部分代码加载到共享内存中,因此最好在分叉之前尽可能多地加载不会更改的代码。(如果代码确实更改了,子进程将收到修改后的内存空间的全新副本,从而减少了共享内存的好处。)这意味着模块需要能够按需预加载它们所需的模块。这就是为什么通常尽可能延迟加载任何内容的 CGI 提供了 :all
选项来一次性加载所有内容。
mod_perl
的人有一套非常出色的文档,说明了持久环境的不同之处,为什么你应该关心,以及你需要做什么来使你的模块正常工作。
重载
创建一个无法与其他重载类一起工作的重载类非常容易。例如,如果我使用 Overload::Num1 和 Overload::Num2,我期望 $num1 + $num2
能按预期工作。不幸的是,由于大多数重载类都像下面这样编写,它们不会这样做。(有关此代码如何工作的更多信息,请阅读 overload
或出色的文章“Overloading。”)
sub add {
my ($l, $r, $inv) = @_;
($l, $r) = ($r, $l) if $inv;
$l = ref $l ? $l->numify : $l;
$r = ref $r ? $r->numify : $r;
return $l + $r;
}
Overload::Num1 使用 numify()
方法检索与类关联的数字。Overload::Num2 使用 get_number()
方法。如果尝试使用这两个类,我会收到一个类似于 无法通过包 “Overload::Num2” 定位对象方法 “numify” 的错误。
解决方案非常简单—不要定义 add()
方法。定义一个 numify
(0+
) 方法,将回退设置为 true,然后走开。您不需要为每个选项定义一个方法。只有在您必须在该操作过程中执行特殊操作时才需要这样做。例如,复杂数必须分别添加有理数和复数部分。
如果您绝对需要定义 add()
,则可以使用类似以下的方法
sub add {
my ($l, $r, $inv) = @_;
($l, $r) = ($r, $l) if $inv;
my $pkg = ref($l) || ref($r);
# This is to explicitly call the appropriate numify() method
$l = do {
my $s = overload::Method( $l, '0+' );
$s ? $s->($l) : $l
};
$r = do {
my $s = overload::Method( $r, '0+' );
$s ? $s->($r) : $r
};
return $pkg->new( $l + $r );
}
这样,每个重载类都可以用自己的方式处理事情。您会注意到,假设是将返回值祝福为调用者调用的类的 add()
。这是可以接受的;有人调用了它的方法,所以 有人 认为它是老大!(如果您定义了 add
方法,没有 numify
方法,并且回退已激活,您将进入无限循环,因为 numify
将回退到 $x + 0
。)
找出某物是什么
在某个时刻,你的模块需要从某个地方接收一些数据。如果你像我一样,你希望你的模块根据接收到的数据自动执行(DWIM)。最终,你想要知道“它是一个标量、数组引用还是散列引用?”(是的,我知道Perl中有七种不同的类型。)有很多方法可以做到这一点,有些甚至可行。
ref()
ref()
是根据数据类型分发的传统方法,产生的代码看起来像my $is_hash = ref( $data ) eq 'HASH';
问题是,如果
$data
是一个对象,那么ref( $data )
将返回$data
的类名。如果有人定义了一个名为HASH
(不要这样做!)的类,并使用blessed数组引用,这将也会非常糟糕地崩溃。isa()
isa()
会告诉你一个引用是否从某个类继承而来。各种数据类型实际上是类样的。有人建议编写如下代码:my $is_hash = UNIVERSAL::isa( $data, 'HASH' );
这将无论
$data
是否blessed都能工作。然而,如果有人恶意地调用一个类HASH
并将数组引用bless到其中,你将会遇到麻烦。更糟糕的是,如果$data
是一个具有重载isa()
方法的对象,这种方法可能会在多态中造成破坏性的影响。eval
块只尝试将数据作为散列引用,看是否成功。
my $is_hash = eval { %{$data}; 1 };
这避免了上述两个选项的主要问题,但意外地可能在重载对象的情况下成功。如果
$data
是Number::Fraction,你将错误地将$data
用作散列,因为Number::Fraction使用blessed散列作为对象,尽管意图是用作标量。假设对象是特殊的
通过使用
Scalar::Util
中的blessed()
和reftype()
函数,你可以确定给定的标量是否是一个blessed引用,或者它实际上是什么类型的引用。如果你想检查某个东西是否是散列引用,但又想避免上述陷阱,编写:my $is_hash = ( !blessed( $data ) && ref $data eq 'HASH' ); # or my $is_hash = reftype( $data ) eq 'HASH';
几乎所有重载的使用都是为了让对象表现得像标量,比如Number::Fraction和类似的类。使用这种技术可以让你更容易地尊重客户端的愿望。你仍然会错过一些可能性,比如(有点古怪)Object::MultiType(如果你用心,Perl中可以做到的很好的一例)。
我的个人偏好是让
$data
告诉你它能够做什么。对象表示
并非所有对象都是blessed散列引用。我喜欢将我的对象表示为数组引用,其他人使用内部-外部对象,它们是引用到undef的引用,与隐藏数据一起工作。这意味着我的重载数字是数组,但我希望你将它们当作标量来对待。除非你询问
$data
如何要求你处理它,否则你如何正确地处理它呢?重载存取器
overload
允许你重载存取器运算符,如@{}
和%{}
。这意味着理论上可以bless一个数组引用,并提供将其作为散列引用访问的能力。Object::MultiType是这样一个例子。它是一个散列引用,提供了类似数组的访问。不幸的是,目前还没有CPAN模块能够做到这一点。
让其他人做你的脏活
我们每天使用的模块通常尽可能的操作系统可移植、版本无关和礼貌。这意味着,你的模块越依赖于其他模块来执行脏活,你就越不需要担心它。像File::Spec和Scalar::Util这样的模块就是为了帮助你。像XML::Parser这样的其他模块将完成它们的工作,但也会处理你遇到的任何Unicode,这样你就不必担心了。
话虽如此,你仍然需要注意你的年轻模块与谁交往。你添加的每个依赖项都是一个可以限制你的模块可以放置的位置的模块。如果你的模块的某个依赖项是Windows专属的,例如来自Win32命名空间的所有内容,那么你的模块现在也仅限于Windows。如果你的某个依赖项有bug,那么你也会有这个bug。幸运的是,有几种方法可以绕过这些问题。
有bug的依赖项
通常,模块作者会相对较快地修复bug,尤其是如果你提供了一个演示bug的测试文件和一个使这些测试通过的补丁。一旦你的模块的依赖项发布了新版本,你就可以发布一个需要带有bug修复功能的新版本。
特定于操作系统的依赖项
第一种选择是接受它。如果Atari MiNT上没有人关心,那么你为什么要关心呢?或者,你可以封装操作系统相关的模块,并找到另一个在你要支持的操作系统上提供相同功能的模块。File::Spec是封装操作系统特定行为在公共API背后的一个很好的例子。
为CPAN编写模块时,有很多需要注意的事项:操作系统和Perl版本、Unicode、线程、持久性——有时可能会非常令人困惑。通过一些简单的步骤,并且愿意让你的用户告诉你他们需要什么,你将成为镇上的热门人物。
标签
反馈
这篇文章有什么问题吗?请通过在GitHub上打开一个问题或拉取请求来帮助我们。