向perl应用程序添加动态代码的最佳方法

我知道此问题的具体实例之前已得到回答:

> How can I dynamically include Perl modules without using eval?
> How do I use a Perl package known only in runtime?

Perl Monks也有很好的答案:

> Writing a Perl module that dynamically loads other modules.
> Creating subroutines on the fly

但是我想要一个强大的方法来为Perl应用程序添加功能:

>高效:如果不需要代码,则不应编译.
>易于调试:如果动态代码出现问题,则报告错误,应指向动态代码的正确位置.
>易于扩展:添加新代码应该像添加新文件或目录文件一样简单.
>易于调用:主应用程序应该能够使用“添加”而不会有太多麻烦.检查“添加”是否已加载以及是否加载它的有效机制将是一个加号.

为了说明这一点,以下是一些可以从一个好的解决方案中受益的例子:

>一组用于从不同应用程序移动数据的脚本.例如,将数据从OpenCart移动到Prestashop,其中数据模型中的每个实体都有一个特定的“添加”来处理输入或输出;然后中间数据模型负责数据的转换.这可用于在任何方向上移动数据,甚至可以在同一电子商务的不同版本之间移动数据.
>需要在不同位置呈现不同类型HTML的Web应用程序.每个“模块”都知道如何处理某些信息并接受参数来完成它.模块输出HTML,另一个输出文档列表,另一个输出文档,另一个输出横幅等.

以下是我使用过的一些例子.

在运行时加载函数并输出可能的编译错误:

eval `cat $file_with_function`;
if( $@ ) {
  print STDERR $@, "\n";
  die "Errors at file $file_with_function\n";
}

或者使用File :: Slurp更健壮:

eval read_file("$file_with_function", binmode => ':utf8');

检查是否已定义某个功能:

if( !defined &myfunction ) {
  die "myfunction is not defined\n";
}

可以从那里调用该函数.这对一个功能很好,但对很多人来说并不适用.

如果函数放在模块中:

require $file_with_function; # needs the ".pm" extension, i.e. addon/func.pm
$name_of_module->import();   # need to know the module name, i.e. Addon::Func

$name_of_module->myfunction(...);

如果需求可以在eval中保护,然后像以前一样使用$@.

随着Module::Load

load $name_of_module;

其次是导入和使用方式相同.安全性不应该是一个问题,因为可以假设动态代码来自受信任的地方.还有更好的方法吗?哪种方式被视为良好做法?

如果它有帮助,我将在Dancer框架内使用解决方案(在其他地方,但不是唯一的).

编辑:鉴于评论,我添加了一些更多的信息.我想到的所有案例都有一个共同点:

>有一个以上的动态代码.可能很多开始.
>每个代码都有相同的接口.

最佳答案 鉴于评论和缺乏回应,我做了一些研究来回答我自己的问题.欢迎提出评论或其他答案!

动态代码

动态代码我指的是在运行时评估的代码.一般来说,我认为编译应用程序会更好,这样在开始执行之前就可以检查Perl编译器可以提供的所有错误.添加使用严格和使用警告,你可以捕获许多常见的错误.那么为什么要使用动态代码呢?这些是我考虑的原因:

>应用程序根据执行的上下文执行许多不同的操作.例如,应用程序从文件中提取某些属性.提取它们的方式取决于文件类型,我们希望处理许多文件类型,但我们不想为我们添加的每种新文件类型更改应用程序.我们还希望应用程序能够快速启动.
>应用程序需要以不需要重新启动应用程序的方式动态扩展.
>我们有一个包含许多功能的大型应用程序.当我们部署应用程序时,我们不希望始终提供所有可能的功能,可能是因为我们单独许可它们,可能是因为并非所有功能都能在所有平台下运行.通过只输入具有我们想要的功能的文件,我们有一个不需要更改任何代码或配置文件的发行版.

我们该怎么做呢?

鉴于Perl提供的可能性,添加动态代码的解决方案有两种形式:使用eval和使用require.然后有一些模块可以帮助以更容易或更易维护的方式做事.

快速而肮脏的方式

eval方式使用表单eval EXPR在运行时编译一段Perl代码.表达式可以是一个字符串,但我建议将代码放在一个文件中,并将其他类似文件分组到一个方便的位置.然后,如果可能的话使用File::Slurp

eval read_file("$file_with_code", binmode => ':utf8');
if( $@ ) {
  die "$file_with_code: error $@\n";
}
if( !defined &myfunction ) {
  die "myfunction is not defined at $file_with_code\n";
}

将字符集指定为read_file可确保正确解释该文件.检查编译是否正确以及我们期望的函数是否已定义也是很好的.所以在$file_with_code中,我们将:

sub myfunction(...) {
  # Do whatever; maybe return something
}

然后你可以正常调用函数.该功能将根据加载的文件而不同.简单而有活力.

模块化方式(推荐)

我考虑可维护性的方式是使用require.与在编译时评估的使用不同,require可以用于在运行时加载模块.在调用require的各种方法中,我会选择:

my $mymodule = 'MyCompany::MyModule'; # The module name ends up in $mymodule
require $mymodule;

与使用不同,require将加载模块但不会执行导入.因此我们可以使用模块内的任何函数,这些函数名称不会调用调用命名空间.要访问该功能,我们需要使用:

$mymodule->myfunction($a, $b);

请参阅下文,了解如何传递参数.这种调用函数的方法将在$a和$b之前添加一个通常名为$self的参数.如果您对面向对象一无所知,可以忽略它.

根据要求将尝试加载模块并且模块可能不存在或者可能无法编译,为了捕获错误,最好使用:

eval "require $mymodule";

然后$@可用于检查加载编译过程中的错误.我们还可以检查函数是否已定义为:

if( $mymodule->can('myfunction') ) {
  die "myfunction is not defined at module $mymodule\n";
}

在这种情况下,我们需要为模块创建一个目录,并为每个模块创建一个扩展名为.pm的文件:

MyCompany
  MyModule.pm

在MyModule.pm里面,我们将:

package MyCompany::MyModule;

sub myfunction {
  my ($self, $a, $b);

  # Do whatever; maybe return something
  # $self will be 'MyCompany::MyModule'
}

1;

package位是必不可少的,它将确保我们放入的任何定义都位于MyCompany :: MyModule命名空间. 1;最后会告诉要求模块初始化是正确的.

如果我们想通过使用其他可以调用调用者名称空间的库来实现该模块,我们可以使用namespace::clean模块.该模块将确保调用者不会从我们定义的模块中获得对命名空间的任何添加.它以这种方式使用:

package MyCompany::MyModule;

# Definitions by these modules will not be available to the code doing the require
use Library1 qw(def1 def2);
use Library2 qw(def3 def4);
...

# Private functions go here and will not be visible from the code doing the require
sub private_function1 {
  ...
}
...

use namespace::clean;

# myfunction will be available
sub myfunction {
  # Do whatever; maybe return something
}
...

1;

如果我们不止一次包含模块会怎样?

简短的回答是什么. Perl会跟踪已加载的模块以及使用%INC变量的位置. use和require都不会加载两次库. use会将任何导出的名称添加到调用者名称空间.要求也不会这样做.如果您想检查模块是否已经加载,您可以使用%INC或更好,您可以使用module::loaded,这是现代Perl版本的核心部分:

use Module::Loaded;

if( !is_loaded( $mymodule ) {
  eval "require $mymodule" );
  ...
}

如何确保Perl找到我的模块文件?

使用和要求Perl使用@INC变量来定义将用于查找库的目录列表.通过将其添加到PERL5LIB环境变量或使用以下命令,可以(通过其他方式)添加新目录.

use lib '/the/path/to/my/libs';

帮手图书馆

我找到了一些库,可用于使使用动态机制的代码更易于维护.他们是:

> if模块:将根据条件加载模块:如果CONDITION,MODULE =>使用ARGUMENTS ;.也可用于卸载模块.
> Module::Load::Conditional:在尝试加载模块时不会死于您,也可能用于检查模块版本或其依赖项.在执行此操作之前,它甚至可以一次性加载模块列表,甚至可以检查它们的版本.

取自Module :: Load :: Conditional文档:

use Module::Load::Conditional qw(can_load);

my $use_list = {
        CPANPLUS        => 0.05,
        LWP             => 5.60,
        'Test::More'    => undef,
};

print can_load( modules => $use_list )
        ? 'all modules loaded successfully'
        : 'failed to load required modules';
点赞