当前位置: 首页 > 工具软件 > CPAN > 使用案例 >

创建优秀的 CPAN 模块

胡弘毅
2023-12-01

创建优秀的 CPAN 模块

译者/作者:luxyi
出处:中国Perl协会 FPC(Foundation of Perlchina)
翻 译:luxyi
审 校:zerray
出 处:中国Perl协会 FPC
原 名:Building Good CPAN Modules
作 者:Rob Kinyon
原 文:http://www.perl.com/pub/a/2005/04/14/cpan_guidelines.html
发 表:April 14, 2005
请保护作者的著作权,维护作者劳动的结晶。

得分:0分 [查看文评]


当你准备向 CPAN 发布一个模块的时候,你要做的第一件事就是要考虑好哪些操作系统, Perl 版本以及其他的一些环境是你所支持和不支持的。通常,这些答案是根据你所提供和不提供的功能来决定的。而这些又根据你所提供的特性以及所用到的模块和库。

然而,很多 CAPN 模块都被无意中限制了其使用范围。不过有许多种步骤可以用来去除这些限制。通常这些步骤是些非常简单的改动,然而它们却能增强你模块的功能和可维护性。

在我的机器上它能工作

你有最新的 PowerBook ,每天都从 CPAN 更新,并且运行最新版本的 Perl 。但使用你模块的人并不是这样。要记住,即便一个应用程序或操作系统比你祖母年纪更大,也并不意味着它再也没用了。代码既不会随着时间的推移而自己产生 bug ,也不会积累垃圾使自己变慢,一些非常重要的应用程序已经在那里运行了三十多年了,还没被做过任何修改。而用来编写这些程序的编程语言是那些你还在襁褓之中时就被人贬低了的。这些应用程序还继续运行着,处理着世界上所有的钱(打个比方)。而且基本上,它们都是运行在非常老的计算机上的。

各个公司都希望继续使用它们现有的旧系统,因为这些系统能够正常工作。它们也希望使用 Perl ,因为 Perl 能在各处运行。如果你使用 CPAN ,那你所有的 Perl 程序就都已经写好了90%了。

入乡随俗

Perl 能够在至少93种不同的操作系统上运行。此外,现在有18种不同的产品化的 Perl 5 版本(还不包括正在开发的分支和不同的编译选择)。93 * 18 = 1674。这就意味着你的模块能够在超过1500种不同的 OS/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 能让你为其构造函数的参数指定一个最低的 Perl 版本。
     

  • 模块/库

    ExtUtils::MakeMaker 中, 你能在调用 WriteMakefile() 函数时设置一个 PREREQ_PM 参数。这个参数指明你的模块需要同其他模块一起运行。这其中能包括版本号,最高版本号或最低版本号都可以。Module::Build 对构造函数的参数有相似的特性。

    如果你依赖于外部的,非 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 和 Windows 上工作,所以我们知道这对于路径分割符和硬编码外部可执行文件上是有缺陷的。然而,还有另外一些问题,它们只会发生在当你的模块被安装到像 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中的理想,5.8.1中作了许多改进。至少我自己没有意识到这一点。一般来说,这些问题对你根本没有影响。

    和安全性修复不同,如果你的模块因为一个以前版本的 Perl 的 bug 而崩溃,那你除了要求使用已经修复了该 bug 的那个版本的 Perl 以外,没有其他什么办法了。

    新特性

    所有人都知道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 新增了语法区域警告。除了对 Perl 可执行文件使用 -w 标记以外,你还可以用 use warnings。由于启用警告一般来说是件好事,要测试一个文件,对于一个使用 Perl 5.6.0+ 的尽责的程序员来说,这是一个非常常见的文件头:

    use strict;
    use warnings;
    
    use Test::More tests => 42;

    现在,即便你的模块能运行在 5.6.0 以前版本的 Perl 上,你的测试代码却不能!这就意味着你的发布内容不能通过 CPAN 或者 CPANPLUS 来安装。对于一个使用这种方式安装模块的管理员,如果他有更好的方法来 debug 一个模块的测试代码,那他就不会安装它。

     

    主要的新特性

    有些新特性非常之大,以至于它们改变了整个游戏的名字。这包括 Unicode 和线程。任何一个 Perl 5 的版本,都在某种形式上对 Unicode 予以支持。那种支持慢慢地从模块(如 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++;
        }
    }

    对于如何成功地把你的应用程序移植到线程中,我所见过的最好的描述就是关于 Perl 5.8 线程的文章 “Where Wizards Fear to Tread”。

     

    Unicode

    虽然在 5.8.0 之前就对 Unicode 有所支持,5.8.0中的一个主要的特性就是在 Perl 中对 Unicode 几乎无缝地处理。在此之前,开发人员得使用 Unicode::String 和其他的模块。 这就意味着,如果你认为为5.8.0版本以前的 Perl 支持 Unicode 很重要,那你就得非常小心地处理字符串。幸运的是,多数主要的模块早就为你做好了这件事,你不用为此担心。

    讨论如何清晰地处理 Unicode 本身就可以写篇文章。请查阅 perlunicodeperluniintro 来获得更多信息。

     

    与其他代码和平相处

    如果你跟我一样,那你曾在幼儿园经常听到“我和其他人处得不好”这句话。虽然这对于一个黑客来说是一个令人景仰的特点,但对于任何一个为商业系统所依赖的模块来说,这没什么好值得骄傲的。要和其他代码和平相处,你得注意不少公共的地方。

    持久化环境

    持久化环境,像 mod_perl 和 FastCGI,是生命的一部分。它们使 WWW 能够正常工作。一般的脚本的工作方式是启动,干它的工作,然后再结束。而持久化环境与之比起来,可以说是个非常不同的怪物。基本上,像 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() 方法。如果我想要一起使用这两个类,我就会收到一个像这样的错误 Can’t locate object method “numify” via package “Overload::Num2”

    要解决这个问题也非常简单——不要定义 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 );
    }

    这样,每个重载类就可以按自己的方式做了。你会注意到,这个假设是把返回值 bless 到被调用 add() 的类中。这一假设是可以接受的,因为他调用了这个方法,那就认为它是主要的!(如果你有一个 add 方法,但没有 numify 方法,并且回调被起用了,那你会进入一个无穷循环中,因为 numify 返回 $x + 0 。)

     

    知道那是什么

     

    从某种角度说,你的模块需要从其他地方获得一些数据。如果你像我一样,那你希望你的模块根据它接受的数据做我希望它做的事情。最终,你想知道,“它到底是个标量,数组引用,还是哈希表引用?”(是,我知道 Perl 中有七种不同的数据类型。)有许多许多种方法来判断,有些甚至还管用。

     

  • ref()

    ref() 是一个历史悠久的基于数据类型来分派不同处理的方式。使用这一方式,代码看起来像这样:

    my $is_hash = ref( $data ) eq 'HASH';

    问题在于,如果 $data 是一个对象,那 ref( $data ) 会返回它的类的名称。如果有人已经通过使用 bless 了的数组引用定义了一个名称叫 HASH 的类(别那样干!),那这会发生很大的问题。

     

  • isa()

    isa() 会告诉你一个引用是否是继承自一个类的,不同的数据类型实际上都类似于类。有些人建议应该这样写代码:

    my $is_hash = UNIVERSAL::isa( $data, 'HASH' );

    不论 $data 被 bless 了与否,这段代码都能够工作。但是,再说一边,如果有人十分卑鄙的定义了一个叫 HASH 的类,还把一个数组引用 bless 进来,那你就有麻烦了。更糟的是,如果 $data 是一个带有重载 isa() 方法的对象时这一技术会严重的破坏多态性。

     

  • evel 块

    试试把数据作为哈希表引用,看看对不对。

    my $is_hash = eval { %{$data}; 1 };

    这避免了上面列出的两个选择的主要问题,但这可能在重载对象时意外地成功。如果 $data 是 Number::Fraction,你会错误地把 $data 当作哈希表使用,因为 Number::Fraction 对对象使用 bless 了的哈希表,尽管这样做是为了把它们当作标量来用。

     

  • 假定对象都是特殊的

    当使用了 Scalar::Util 的 blessed() 和 reftype() 函数,你就能够知道一个标量是否是 bless 了的引用或这个引用的真正的类型。如果你想知道某个变量是否是哈希表引用,但你想避免以上列出来的陷阱,你可以这样写:

    my $is_hash = ( !blessed( $data ) && ref $data eq 'HASH' );
    # or
    my $is_hash = reftype( $data ) eq 'HASH';

    几乎所有重载的使用都是为了使一个对象可以像一个标量那用被使用,就像 Number::Fraction 和类似的类。使用这一技术可以使你更容易地尊重客户的愿望。但你仍然会失去一些可能性,比如(有些古怪的)Object::MultiType (一个很好的例子,告诉你如果用心,能用 Perl 做些什么)。

    我个人的喜好是让 $data 来告诉你它能做些什么。

     

  • 对象的表现

    不是所有的对像都被 bless 为哈希表引用。我喜欢把我的对象表现为数组引用,还有些人使用反对象,这些是 undef 的引用,通常和隐藏数据一起使用。这意味着,我的重载的数字是数组,但我希望你把它们当作标量来用。除非你去查询 $data 看到底该如何使用它,否则你如何才能正确使用它呢?

     

  • 重载 accessor

    overload 允许你重载 accessor 操作符,如 @{} 和 %{}。这意味着它可以理论上被 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,线程,持久化——有时它会让你发疯的。但只要采用了这些简单的步骤,并且愿意让你的用户告诉你他们需要什么,你就会受人欢迎的。

 类似资料: