将动态代码添加到 Perl 应用程序的最佳方法

Jav*_*ces 5 perl

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

Perl Monks 也有很好的答案:

但我想要一种强大的方法来向 Perl 应用程序添加功能,即:

  1. 高效:如果代码不需要,则不应编译。
  2. 易于调试:如果动态代码出现问题,则错误报告应该指向动态代码的正确位置。
  3. 易于扩展:添加新代码应该像添加新文件或目录+文件一样简单。
  4. 易于调用:主应用程序应该能够轻松使用“附加组件”。一个有效的机制来检查“附加”是否已经加载,如果没有加载,将是一个优点。

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

  • 一组从不同应用程序移动数据的脚本。例如,将数据从 OpenCart 移动到 Prestashop,其中数据模型中的每个实体都有一个处理输入或输出的特定“附加组件”;然后中间数据模型负责数据的转换。这可用于在任何方向甚至同一电子商务的不同版本之间移动数据。

  • 需要在不同位置呈现不同类型 HTML 的 Web 应用程序。每个“模块”都知道如何处理特定信息并接受参数来执行此操作。一个模块输出 HTML,另一个模块输出文档列表,另一个模块输出文档,另一个模块输出横幅,等等。

以下是我使用过并且有效的一些示例。

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

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

或者使用更强大的File::Slurp

eval read_file("$file_with_function", binmode => ':utf8');
Run Code Online (Sandbox Code Playgroud)

检查某个函数是否已定义:

if( !defined &myfunction ) {
  die "myfunction is not defined\n";
}
Run Code Online (Sandbox Code Playgroud)

可以从那里调用该函数。这对于一种功能来说没问题,但对于很多功能来说就不行了。

如果将函数放入模块中:

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(...);
Run Code Online (Sandbox Code Playgroud)

其中require可以将其保护在内部eval,然后$@像以前一样使用。

使用模块::加载

load $name_of_module;
Run Code Online (Sandbox Code Playgroud)

后面跟着 和import的用法相同。安全性不应成为问题,因为可以假设动态代码来自受信任的地方。还有更好的方法吗?哪种方式被认为是好的做法

如果它有帮助,我将在Dancer框架内使用该解决方案(以及其他地方,但不限于此)。

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

  1. 有不止一段动态代码。可能有很多开始。
  2. 每一位代码都有相同的接口

Jav*_*ces 1

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

\n\n

动态代码

\n\n

我所说的动态代码是指在运行时评估的代码。一般来说,我认为最好编译应用程序,以便在开始执行之前进行 Perl 编译器可以提供的所有错误检查。添加到use strictuse warnings,您可以通过这种方式发现许多常见错误。那么为什么要使用动态代码呢?我认为的原因如下:

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

我们该怎么做呢?

\n\n

鉴于 Perl 提供的可能性,添加动态代码的解决方案有两种:使用eval和使用require. 还有一些模块可以帮助您以更简单或更易于维护的方式完成任务。

\n\n

快速而肮脏的方法

\n\n

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

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

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

\n\n
sub myfunction(...) {\n  # Do whatever; maybe return something\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后就可以正常调用该函数了。该函数将根据加载的文件而有所不同。简单而动态。

\n\n

模块化方式(推荐)

\n\n

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

\n\n
my $mymodule = \'MyCompany::MyModule\'; # The module name ends up in $mymodule\nrequire $mymodule;\n
Run Code Online (Sandbox Code Playgroud)\n\n

也不像userequire会加载模块但不会执行import。因此,我们可以使用模块内的任何函数,并且这些函数名称不会污染调用名称空间。要访问该功能,我们需要使用:

\n\n
$mymodule->myfunction($a, $b);\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

由于require将尝试加载模块,并且该模块可能不存在或可能无法编译,因此要捕获错误,最好使用:

\n\n
eval "require $mymodule";\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

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

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

\n\n
MyCompany\n  MyModule.pm\n
Run Code Online (Sandbox Code Playgroud)\n\n

里面MyModule.pm我们将有:

\n\n
package MyCompany::MyModule;\n\nsub myfunction {\n  my ($self, $a, $b);\n\n  # Do whatever; maybe return something\n  # $self will be \'MyCompany::MyModule\'\n}\n\n1;\n
Run Code Online (Sandbox Code Playgroud)\n\n

package位是必不可少的,它将确保我们放入的任何定义都位于MyCompany::MyModule名称空间中。最后1;将表明require模块初始化是正确的。

\n\n

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

\n\n
package MyCompany::MyModule;\n\n# Definitions by these modules will not be available to the code doing the require\nuse Library1 qw(def1 def2);\nuse Library2 qw(def3 def4);\n...\n\n# Private functions go here and will not be visible from the code doing the require\nsub private_function1 {\n  ...\n}\n...\n\nuse namespace::clean;\n\n# myfunction will be available\nsub myfunction {\n  # Do whatever; maybe return something\n}\n...\n\n1;\n
Run Code Online (Sandbox Code Playgroud)\n\n

如果我们多次包含一个模块会发生什么?

\n\n

简短的回答是什么都没有。Perl 使用变量跟踪哪些模块已加载以及从何处加载%INC。两者userequire不会加载库两次。use会将所有导出的名称添加到调用者名称空间中。require也不会那样做。如果你想检查模块是否已经加载,你可以使用%INC或者更好,你可以使用module::loaded,它是现代 Perl 版本核心的一部分:

\n\n
use Module::Loaded;\n\nif( !is_loaded( $mymodule ) {\n  eval "require $mymodule" );\n  ...\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

ForuserequirePerl 使用该@INC变量来定义将用于查找库的目录列表。向其中添加新目录可以通过将其添加到PERL5LIB环境变量或使用以下方法来实现(以及其他方式):

\n\n
use lib \'/the/path/to/my/libs\';\n
Run Code Online (Sandbox Code Playgroud)\n\n

辅助库

\n\n

我发现了一些库,可以用来使使用动态机制的代码更易于维护。他们是:

\n\n
    \n
  • if模块:将根据条件加载或不加载模块:use if CONDITION, MODULE => ARGUMENTS;。也可用于卸载模块。
  • \n
  • Module::Load::Conditional:在尝试加载模块时不会死掉,也可用于检查模块版本或其依赖项。它还能够一次加载所有模块列表,甚至在加载之前检查它们的版本。
  • \n
\n\n

摘自 Module::Load::Conditional 文档:

\n\n
use Module::Load::Conditional qw(can_load);\n\nmy $use_list = {\n        CPANPLUS        => 0.05,\n        LWP             => 5.60,\n        \'Test::More\'    => undef,\n};\n\nprint can_load( modules => $use_list )\n        ? \'all modules loaded successfully\'\n        : \'failed to load required modules\';\n
Run Code Online (Sandbox Code Playgroud)\n