【问题标题】:Best way to add dynamic code to a perl application将动态代码添加到 perl 应用程序的最佳方法
【发布时间】:2018-04-06 11:31:23
【问题描述】:

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

Perl Monks 也有很好的答案:

但我想要一种向 Perl 应用程序添加功能的可靠方法:

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

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

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

  • 需要在不同位置呈现不同类型 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(...);

require 可以在eval 中受到保护,然后像以前一样使用$@

Module::Load:

load $name_of_module;

后跟import 并以相同的方式使用。 安全性不应成为问题,因为可以假定动态代码来自受信任的地方。有没有更好的方法?哪种方式会被视为良好做法

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

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

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

【问题讨论】:

  • 你想解决什么问题?
  • 我举了两个例子,但可能不太清楚。问题是一种在不接触其代码的情况下向应用程序添加新功能的方法。例如,通过向新文件/目录添加新代码。
  • 一个好的解决方案将允许应用程序知道它的新功能并使用它们,而应用程序的用户可能只能参考他们。
  • 那是一种技术的应用。你说你不希望代码预先编译。为什么?你想在那里解决什么问题?在我看来,你在试图过度设计,但你最终会变得聪明过头,而且你会背负一大堆技术债务,你的同事会恨你。
  • require "dir/perlscript.NotPmExtension"; 不必是包内的代码。

标签: perl


【解决方案1】:

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

动态代码

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

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

我们怎么做?

鉴于 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。与use编译时 评估不同,require 可用于在运行时 加载模块。在调用require 的各种方式中,我会选择:

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

也不同于userequire 将加载模块但不会执行import。所以我们可以使用模块内的任何函数,这些函数名不会污染调用命名空间。要访问我们需要使用的功能:

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

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

由于require 会尝试加载一个模块并且该模块可能不存在或者它可能无法编译,所以最好使用捕获错误:

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; 会告诉require 模块初始化是正确的。

如果我们想通过使用可能污染调用方命名空间的其他库来实现该模块,我们可以使用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 变量跟踪已加载的模块以及从何处加载。 userequire 都不会加载库两次。 use 会将所有导出的名称添加到调用者命名空间。 require 也不会这样做。如果您想检查一个模块是否已经加载,您可以使用%INC 或者更好,您可以使用module::loaded,它是现代 Perl 版本中核心的一部分:

use Module::Loaded;

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

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

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

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

助手库

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

  • if 模块:将根据条件加载或不加载模块:use 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';

【讨论】:

    猜你喜欢
    • 2021-03-05
    • 1970-01-01
    • 2013-07-13
    • 2010-11-07
    • 1970-01-01
    • 2014-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多