使用别名来要求Perl模块

问题描述 投票:5回答:5

***以下是帮助解释我到目前为止尝试过的背景。如果您想首先阅读主要问题,请跳至最底层。***


Starting Out

我的Baz模块调用了许多其他类似的模块,每个模块都在命名空间中向下一级。这里感兴趣的人组成了Thing角色。除了单独的require语句之外,常量列表ALL_THINGS列举了相关的Thing模块,供以后使用。我的原始代码如下:

package Foo::Bar::Baz;

use constant ALL_THINGS => qw{ Foo::Bar::Baz::ThingA Foo::Bar::Baz::ThingB ... };

require Foo::Bar::Baz::ThingA;
require Foo::Bar::Baz::ThingB;
[...]

Eliminating Redundancy

正如我所提到的,有很多Thing模块,我还在增加更多。每次我创建一个新的Thing类时,我都要添加一个新的require语句,并在ALL_THINGS列表中添加相同的相同文本。为了避免这种重复,我想用循环迭代require替换单独的ALL_THINGS线。我添加了这个,它本身很好用:

foreach my $module (ALL_THINGS) {
    eval "require $module";
}

然而,这个解决方案似乎不适合我的下一次改变。


Improving Readability

每个Thing的完整模块名称很长且很笨重。我想对包名称进行别名,以便更容易输入/读取。我看着Package::Alias,但似乎use他们,我想尽可能避免。到目前为止,我遇到的最佳解决方案是this question建议的模式:

BEGIN { *Things:: = *Foo::Bar::Baz:: ; }

这也有效,因为它允许我使用Thing::ThingA->classMethod。然而,不出所料,它在上面的require循环中不起作用,因为require Thing::ThingA@INC搜索Thing/ThingA.pm而不是Foo/Bar/Baz/ThingA.pm


Main Question: Putting Them Together

我想将Foo::Bar::Baz::ThingA列表中的长包名称(即ALL_THINGS)减少到Things::ThingA,但仍然可以使用相同的列表在循环中构建我的require语句。

  • 有没有不同的方法将Foo::Bar::Baz::别名为Things::,以便我可以require Things::ThingA
  • 或者,如果我正在做别名部分,是否有办法将Things::ThingA解除引用到Foo::Bar::Baz::ThingA(或之前?)eval,以便require找到正确的包?
  • 是否有一些其他普遍接受的方法将包绑定在同一命名空间的不同级别,以避免所有这些的需要?

奖金问题(与eval "require $x"有关):

  • perldoc for constant中,它表示常量列表实际上不是只读的。这是否会因使用eval而引起安全问题?
  • 如果是这样,有没有更安全的方式来完成它而无需加载额外的模块?
  • 对Perl来说有些新东西,在这种方法和我之前的方法(每个模块的单独的require语句)之间,我可能会错过任何细微的差异吗?

注意:我接受了Dave Sherohman的回答,因为它最能完全解决我提出的问题。但是,我最终根据lordadmira的答案实施了一个解决方案。

perl
5个回答
3
投票

你觉得你的魔法有多黑?

我们都知道,为了require模块,Perl通过@INC查找它想要加载的文件。这个过程鲜为人知(甚至更少使用)的一个方面是@INC不仅限于包含文件系统路径。您还可以在其中放置coderefs,允许您劫持模块加载过程并将其弯曲到您的意愿。

对于您所描述的用例,类似以下内容(未经测试)应该可以解决这个问题:

BEGIN { unshift @INC, \&require_things }

sub require_things {
  my (undef, $filename) = @_;

  # Don't go into an infinite loop when you hit a non-Thing:: module!
  return unless $filename =~ /^Thing::/;

  $filename =~ s/^Thing::/Foo::Bar::Baz::/;
  require $filename;  
}

基本上它的作用是,作为@INC中的第一个条目,它查看所请求模块的名称,如果它以Thing::开头,则会加载相应的Foo::Bar::Baz::模块。简单有效,但很容易混淆未来的维护程序员(包括你自己!),所以要谨慎使用。


作为一种替代方法,您还可以选择在模块中指定一个与文件的物理路径不对应的包名称 - 这两个通常按照惯例相同,以便在阅读和维护代码时更轻松,但没有技术要求他们匹配。如果文件./lib/Foo/Bar/Baz/Xyzzy.pm包含

package Thing::Xyzzy;

sub frob { ... };

然后你会通过这样做来使用它

require Foo::Bar::Baz::Xyzzy;
Thing::Xyzzy::frob();

而Perl对此非常满意(即使你的同事可能不是这样)。


最后,如果你想摆脱ALL_THINGS,请看看Module::Pluggable。您给它一个命名空间,然后它找到该命名空间中的所有可用模块,并为您提供它们的列表。它也可以设置为require每个模块,因为它被发现:

use Module::Pluggable require => 1, search_path => ['Foo::Bar::Baz'];
my @plugins = plugins;

@plugins现在包含所有Foo::Bar::Baz::*模块的列表,这些模块已经加载了require。或者你可以调用plugins而不将结果赋给变量,如果你只关心加载模块而不需要它们的列表。


3
投票

是否有一种不同的方法来将Foo :: Bar :: Baz :: as ::作为Things ::这样我可以要求Things :: ThingA?

是。这有两个要求:

  1. 像您已经完成的那样创建包别名。 BEGIN { *Things:: = *Foo::Bar::Baz:: }
  2. mylibs/Things目录创建一个指向mylibs/Foo/Bar/Baz的符号链接(其中mylibs是Perl模块的路径) (如果你愿意的话,也可以从Foo/Bar/Baz.pm文件链接到Things.pm

完成此操作并调用eval "require Things::Quux"eval "use Things::Quux"后,Perl将在mylibs/Things/Quux.pm中加载该文件,该文件与mylibs/Foo/Bar/Baz/Quux.pm文件相同。该文件中包含package Foo::Bar::Baz::Quux语句,但由于该包已经被别名化为Things::Quux名称空间,因此所有子程序和包变量都可以在任一名称空间中访问。

是否有一些其他普遍接受的方法将包绑定在同一命名空间的不同级别,以避免所有这些的需要?

目前还不清楚你的对象模型是什么,但如果*::Thing1*::Thing2等都是某些常见基类的实现,你可以考虑基类中的工厂方法。

package Foo::Bar::Baz;
sub newThing {
    my ($class, $implementation, @options) = @_;
    eval "use $class\::$implementation; 1"
        or die "No $implementation subclass yet";
    no strict 'refs';
    my $obj = "$class\::$implementation"->new(@options);
    return $obj;
}

现在Foo::Bar::Baz::Thing7(可能或可能没有别名为Things::Thing7)只有在需要时才会被加载,比如来自像

my $obj7 = Foo::Bar::Baz->newThing("Thing7",foo => 42);
print ref($obj7);   # probably  Foo::Bar::Baz::Thing7

2
投票

与typeglobs混淆就像这里的核过度杀伤一样。 Module :: Runtime是基于配置数据在运行时加载模块的标准方法。在这一点上,一切都可以是普通变量。在这里使用常数没有任何好处。

我的建议来自我们的IRC聊天。

package Foo::Bar::Baz;

use strict;
use Module::Runtime "require_module";
use List::Util "uniq";

my $prefix = "Things::LetterThings";
my %prop_module_map = (
   foo => [ qw{ThingC ThingF ThingY} ],
   bar => [ qw{ThingL ThingB} ],
   baz => [ qw{ThingA ThingB ThingC ThingE ThingG ThingH ThingZ} ],
   # or
   # ALL => [ qw{ThingA .. ThingZ} ],
);
my @all_modules = uniq map { @$_ } values %prop_module_map;

sub load_modules {
  my $self = shift;

  # map module list if matching property found, otherwise use ALL_MODULES
  my $modules = $prop_module_map{$self->prop} ?
     $prop_module_map{$self->prop} :
     \@all_modules;

  #only do the map operation on the list we actually need to use
  my @modules = map { join "::", $prefix, $_  } @$modules;

  foreach my $module (@modules) {
    require_module($module);
  }
}

1;
__END__

0
投票

我提出了一个基于perlmod中的例子的解决方案似乎有效,尽管我仍然试图绕过它。我发布它是希望有人可以改进它,或者至少解释它/提供反馈。

foreach my $package (ALL_THINGS) {
    no strict "refs";
    $package = *{$package}{PACKAGE}."::".*{$package}{NAME};
    eval "require $package";
}

编辑:在链接到perlref之后,我在第(7)节中找到了这个模糊:

* foo {NAME}和* foo {PACKAGE}是例外,因为它们返回字符串而不是引用。它们返回typeglob本身的包和名称,而不是已分配给它的包。因此,在* foo = * Foo :: bar之后,* foo在用作字符串时将变为“* Foo :: bar”,但* foo {PACKAGE}和* foo {NAME}将继续生成“main”和“ foo“,分别。

从那以后,*Things{PACKAGE}将始终解析为Foo::Bar::Baz是有道理的,因为那是我们正在处理的包,因此是typeglob“属于”的包。在上面的代码中,$package解析为Things::ThingA,而不是Things,所以我们得到*Things::ThingA{PACKAGE}。同样,第二部分成为*Things::ThingA{NAME}。我可能会冒一些猜测,为什么这可能有效,但事实是我不确定。


0
投票

NAME和PACKAGE主要是为了让您可以找到参考文件的来源和名称。除此之外,还有一种方法可以返回变量名而不是变量值。调试。

printf "%s\n", __PACKAGE__; my $y = \*ydp; pp *$y{PACKAGE}, *$y{NAME};
W
("W", "ydp")
© www.soinside.com 2019 - 2024. All rights reserved.