在 Perl 中处理共享公共 "ancestor" 的模块的多重继承的正确方法是什么?
What's the correct way to deal with multiple inheritence of modules sharing a common "ancestor" in Perl?
(Moose/Moo 的答案当然是“角色”。这个问题是关于一般情况的,在这种情况下,您想要组合两个属于同一父级的子class 的模块, 假设没有 Moose/Moo.)
让我们举一个稍微做作的例子:模块 LWP::UserAgent::Determined
and LWP::RobotUA
are both subclasses of LWP::UserAgent
并以不同的方式扩展它。如果我想创建一个结合了两者方法的对象怎么办?它的核心仍然是一个 LWP::UserAgent
对象,其他两个模块不会相互冲突,所以应该很容易,对吧?
据我所知,正确的做法是创建一个新包,将另外两个包声明为父包——use parent qw(LWP::RobotUA LWP::UserAgent::Determined)
——然后从中创建对象。而且,事实上,如果你这样做,你会得到一个对象,它包含来自两者的方法,以及来自基础 class LWP::UserAgent
和 almost 一切的方法正如您所期望的那样工作。
但不完全是。如果没有给出其他值,LWP::UserAgent::Determined
和 LWP::RobotUA
都有某些属性的默认值,这些属性是在创建对象时设置的。将两者组合时,LWP::RobotUA
的默认值得到设置,但 不是 LWP::UserAgent::Determined
的。所以一定是哪里出了问题。
这是一些测试代码:
#!/usr/bin/env perl
use strict;
use warnings;
use 5.016;
use LWP::RobotUA;
use LWP::UserAgent::Determined;
package MyUA;
use parent qw(LWP::RobotUA LWP::UserAgent::Determined);
for my $module (qw(LWP::RobotUA LWP::UserAgent::Determined MyUA)) {
say '# ', $module, ' #';
my $ua = $module->new(
'agent' => 'Test-UA',
'from' => 'example@example.com',
);
my $req = HTTP::Request->new(GET => 'https://www.bbc.co.uk/emp/network_status.txt');
my $response = $ua->request($req);
unless ($module eq 'LWP::UserAgent::Determined') {
say 'Use sleep? : ', $ua->use_sleep() // 'not defined!';
say 'Allowed OK? : ', $ua->rules->allowed('https://www.bbc.co.uk/') // 'not defined!';
say 'Sites with rules: ', (defined $ua->rules()->{loc}) ? join(', ', (sort keys %{$ua->rules()->{loc}})) : 'not defined!';
}
unless ($module eq 'LWP::RobotUA') {
print 'Timings: ';
if (defined $ua->timing()) {
say $ua->timing();
}
else {
print 'Timing defaults not set! ';
$ua->timing('1,5,10,20,60,240');
say '...but the method works: ', $ua->timing();
}
say 'Retry codes: ', (defined $ua->codes_to_determinate()) ? join(', ', (sort keys %{$ua->codes_to_determinate()})) : 'not defined!';
}
say '#'x60;
}
这输出:
#LWP::RobotUA#
用睡眠? : 1
可以吗? : 1
有规则的站点:www.bbc.co.uk:443
################################################## ##########
#LWP::UserAgent::Determined#
时间:1,3,15
重试代码:408、500、502、503、504
################################################## ##########
#我的UA#
用睡眠? : 1
可以吗? : 1
有规则的站点:www.bbc.co.uk:443
时间:未设置时间默认值! ...但该方法有效:1,5,10,20,60,240
重试代码:未定义!
################################################## ##########
在这里您可以看到两个模块的方法都有效,但是当与 [=13= 结合使用时,LWP::UserAgent::Determined
的 timing()
或 codes_to_determinate()
方法没有设置默认值],而 LWP::RobotUA
的 use_sleep()
方法 是 使用其默认值 1
创建的。但是,手动设置值可以正常工作,否则组合对象将按预期工作。
因此,总而言之:处理这种情况的正确方法是什么,您想要合并两个模块,子class 共同的第三个模块?这实际上是正确的吗,但我只是选择了一个不幸的例子,LWP::UserAgent::Determined
在设置默认值方面表现不佳?
您的新 class 实际上看起来像这样:
package MyUA;
use parent qw(LWP::RobotUA LWP::UserAgent::Determined);
1;
让我们这样测试:
#!/usr/bin/perl
use strict;
use warnings;
use feature 'say';
use MyUA;
my $ua = MyUA->new(
agent => 'test',
from => 'me@example.com',
);
say ref $ua;
这告诉我们有“MyUA”对象。但它到底是什么?我们做了什么?
好吧,对象是使用构造函数方法构建的。这(通常)称为 new()
。在这种情况下,您还没有在 class 中定义 new()
方法。所以 Perl 会在 superclasses 中寻找方法。它通过搜索在 @INC
中找到的 classes 并查看每个是否依次包含 new()
方法来做到这一点。
你的两个超级classes 都有一个new()
方法。但是 Perl 只需要一个。因此,当它找到一个时,它会停止查找并调用该方法。它调用的第一个是 LWP::RobotUA 中的那个(因为这是传递给 use parent
的列表中的第一个)。这样就可以调用了。
这意味着您实际上得到的是classLWP::RobotUA的一个对象。好吧,主要是。它被赋予了正确的 class,如果你调用了 LWP::UserAgent::Determined 中的任何方法,但不在 LWP::RobotUA 中,它仍然有效。但是 LWP::UserAgent::Determined 初始化代码的 none 已经被调用了。
这很好地说明了为什么多重继承不是一个好主意。除了最微不足道的情况外,很难在所有情况下都做到正确。
我不能在这里给你答案。因为只有你知道你需要两个 superclasses 中的哪些位。但解决方案将涉及将您自己的 new()
方法添加到您的 class 并可能从中调用两个 superclass 构造函数。
更新: 好的,我仔细看了看。而且它可能比我想象的要容易。
LWP::RobotUA::new()
在做各种其他事情的过程中调用 LWP::UserAgent::new()
。但是 LWP::UserAgent::Determined::new()
在其处理开始时调用 LWP::UserAgent::new()
,然后将其所有其他初始化捆绑在一个名为 _determined_init()
.
的单独方法中。
所以看起来您的解决方案可能很简单,只需添加一个构造方法,如下所示:
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
$self->_determined_init();
return $self;
}
对 $class->SUPER::new()
的调用会调用 LWP::RobotUA::new()
,因为这是 @INC
中的第一个 class。反过来,调用 LWP::UserAgent::new()
- 这样初始化就完成了。然后我们只需要调用 _determined_init()
来初始化另一个 superclass.
它似乎在我的(非常基本的)测试中起作用。但我仍然对多重继承持怀疑态度:-)
更新 2: 是的。 。我的解决方案只解决了构建对象的问题。我没有考虑实际使用它。
如果不更改 LWP::UA::Determined and/or LWP::RobotUA.
,那将无法工作
但是这些更改可以通过猴子修补来应用。底部给出了解决方案。
问题
LWP::UA::Determined 包装了 ->simple_request
,也就是说它提供了一个 ->simple_request
调用 ->SUPER::simple_request
(即 ->LWP::UA::simple_request
)
LWP::RobotUA 包装了 ->simple_request
,也就是说它提供了一个 ->simple_request
调用 ->SUPER::simple_request
(即 ->LWP::UA::simple_request
)
当您调用 ->request
时,您最终会调用 ->simple_request
。根据 @ISA
中模块的顺序,这将调用 LWP::UA::Determined::simple_request
(然后 LWP::UA::simple_request
)或 LWP::RobotUA::simple_request
(然后 LWP::UA::simple_request
)。
它不会同时调用 LWP::UA::Determined::simple_request
和 LWP::RobotUA::simple_request
。如果它这样做了,它就不会工作,因为那最终会调用 LWP::UA::simple_request
两次。
->new
存在完全相同的问题。
演示
use feature qw( say );
{ package Base;
sub method { say __PACKAGE__; } }
{ package SubA; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->SUPER::method(); } }
{ package SubB; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->SUPER::method(); } }
{ package SubAB; our @ISA = qw( SubA SubB ); }
SubAB->method(); # SubA Base
解决方案
LWP::UA::Determined 和 LWP::RobotUA 需要使用 next::method
而不是 SUPER
(或者它们需要完全不同的设计),这样才有机会工作。
演示
use feature qw( say );
{ package Base;
sub method { say __PACKAGE__; } }
{ package SubA; use mro 'c3'; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->next::method(); } }
{ package SubB; use mro 'c3'; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->next::method(); } }
{ package SubAB; use mro 'c3'; our @ISA = qw( SubA SubB ); }
SubAB->method(); # SubA SubB Base
解决方法
所有的希望都没有破灭!我们可以猴子修补 LWP::UA::Determined 和 LWP::RobotUA 以获得所需的行为!
只需将此添加到您的程序中:
use LWP::RobotUA qw( );
BEGIN {
package LWP::RobotUA::Inserted;
use mro 'c3';
our @ISA = @LWP::RobotUA::ISA;
sub new { return $_[0]->next::method(); }
sub simple_request { return $_[0]->next::method(); }
sub agent { return $_[0]->next::method(); }
@LWP::RobotUA::ISA = __PACKAGE__;
}
use LWP::UserAgent::Determined qw( );
BEGIN {
package LWP::UserAgent::Determined::Inserted;
use mro 'c3';
our @ISA = @LWP::UserAgent::Determined::ISA;
sub new { return $_[0]->next::method(); }
sub simple_request { return $_[0]->next::method(); }
@LWP::UserAgent::Determined::ISA = __PACKAGE__;
}
您还需要 use mro 'c3';
在您自己的 class 中(或仔细选择 @ISA
的顺序)才能使 ->agent
正常工作。
演示
# Insert code from first demo here.
{ package SubA::Inserted; use mro 'c3'; our @ISA = @SubA::ISA;
sub method { return $_[0]->next::method(); }
@SubA::ISA = __PACKAGE__; }
{ package SubB::Inserted; use mro 'c3'; our @ISA = @SubB::ISA;
sub method { return $_[0]->next::method(); }
@SubB::ISA = __PACKAGE__; }
SubAB->method(); # SubA SubB Base
(Moose/Moo 的答案当然是“角色”。这个问题是关于一般情况的,在这种情况下,您想要组合两个属于同一父级的子class 的模块, 假设没有 Moose/Moo.)
让我们举一个稍微做作的例子:模块 LWP::UserAgent::Determined
and LWP::RobotUA
are both subclasses of LWP::UserAgent
并以不同的方式扩展它。如果我想创建一个结合了两者方法的对象怎么办?它的核心仍然是一个 LWP::UserAgent
对象,其他两个模块不会相互冲突,所以应该很容易,对吧?
据我所知,正确的做法是创建一个新包,将另外两个包声明为父包——use parent qw(LWP::RobotUA LWP::UserAgent::Determined)
——然后从中创建对象。而且,事实上,如果你这样做,你会得到一个对象,它包含来自两者的方法,以及来自基础 class LWP::UserAgent
和 almost 一切的方法正如您所期望的那样工作。
但不完全是。如果没有给出其他值,LWP::UserAgent::Determined
和 LWP::RobotUA
都有某些属性的默认值,这些属性是在创建对象时设置的。将两者组合时,LWP::RobotUA
的默认值得到设置,但 不是 LWP::UserAgent::Determined
的。所以一定是哪里出了问题。
这是一些测试代码:
#!/usr/bin/env perl
use strict;
use warnings;
use 5.016;
use LWP::RobotUA;
use LWP::UserAgent::Determined;
package MyUA;
use parent qw(LWP::RobotUA LWP::UserAgent::Determined);
for my $module (qw(LWP::RobotUA LWP::UserAgent::Determined MyUA)) {
say '# ', $module, ' #';
my $ua = $module->new(
'agent' => 'Test-UA',
'from' => 'example@example.com',
);
my $req = HTTP::Request->new(GET => 'https://www.bbc.co.uk/emp/network_status.txt');
my $response = $ua->request($req);
unless ($module eq 'LWP::UserAgent::Determined') {
say 'Use sleep? : ', $ua->use_sleep() // 'not defined!';
say 'Allowed OK? : ', $ua->rules->allowed('https://www.bbc.co.uk/') // 'not defined!';
say 'Sites with rules: ', (defined $ua->rules()->{loc}) ? join(', ', (sort keys %{$ua->rules()->{loc}})) : 'not defined!';
}
unless ($module eq 'LWP::RobotUA') {
print 'Timings: ';
if (defined $ua->timing()) {
say $ua->timing();
}
else {
print 'Timing defaults not set! ';
$ua->timing('1,5,10,20,60,240');
say '...but the method works: ', $ua->timing();
}
say 'Retry codes: ', (defined $ua->codes_to_determinate()) ? join(', ', (sort keys %{$ua->codes_to_determinate()})) : 'not defined!';
}
say '#'x60;
}
这输出:
#LWP::RobotUA# 用睡眠? : 1 可以吗? : 1 有规则的站点:www.bbc.co.uk:443 ################################################## ########## #LWP::UserAgent::Determined# 时间:1,3,15 重试代码:408、500、502、503、504 ################################################## ########## #我的UA# 用睡眠? : 1 可以吗? : 1 有规则的站点:www.bbc.co.uk:443 时间:未设置时间默认值! ...但该方法有效:1,5,10,20,60,240 重试代码:未定义! ################################################## ##########
在这里您可以看到两个模块的方法都有效,但是当与 [=13= 结合使用时,LWP::UserAgent::Determined
的 timing()
或 codes_to_determinate()
方法没有设置默认值],而 LWP::RobotUA
的 use_sleep()
方法 是 使用其默认值 1
创建的。但是,手动设置值可以正常工作,否则组合对象将按预期工作。
因此,总而言之:处理这种情况的正确方法是什么,您想要合并两个模块,子class 共同的第三个模块?这实际上是正确的吗,但我只是选择了一个不幸的例子,LWP::UserAgent::Determined
在设置默认值方面表现不佳?
您的新 class 实际上看起来像这样:
package MyUA;
use parent qw(LWP::RobotUA LWP::UserAgent::Determined);
1;
让我们这样测试:
#!/usr/bin/perl
use strict;
use warnings;
use feature 'say';
use MyUA;
my $ua = MyUA->new(
agent => 'test',
from => 'me@example.com',
);
say ref $ua;
这告诉我们有“MyUA”对象。但它到底是什么?我们做了什么?
好吧,对象是使用构造函数方法构建的。这(通常)称为 new()
。在这种情况下,您还没有在 class 中定义 new()
方法。所以 Perl 会在 superclasses 中寻找方法。它通过搜索在 @INC
中找到的 classes 并查看每个是否依次包含 new()
方法来做到这一点。
你的两个超级classes 都有一个new()
方法。但是 Perl 只需要一个。因此,当它找到一个时,它会停止查找并调用该方法。它调用的第一个是 LWP::RobotUA 中的那个(因为这是传递给 use parent
的列表中的第一个)。这样就可以调用了。
这意味着您实际上得到的是classLWP::RobotUA的一个对象。好吧,主要是。它被赋予了正确的 class,如果你调用了 LWP::UserAgent::Determined 中的任何方法,但不在 LWP::RobotUA 中,它仍然有效。但是 LWP::UserAgent::Determined 初始化代码的 none 已经被调用了。
这很好地说明了为什么多重继承不是一个好主意。除了最微不足道的情况外,很难在所有情况下都做到正确。
我不能在这里给你答案。因为只有你知道你需要两个 superclasses 中的哪些位。但解决方案将涉及将您自己的 new()
方法添加到您的 class 并可能从中调用两个 superclass 构造函数。
更新: 好的,我仔细看了看。而且它可能比我想象的要容易。
LWP::RobotUA::new()
在做各种其他事情的过程中调用 LWP::UserAgent::new()
。但是 LWP::UserAgent::Determined::new()
在其处理开始时调用 LWP::UserAgent::new()
,然后将其所有其他初始化捆绑在一个名为 _determined_init()
.
所以看起来您的解决方案可能很简单,只需添加一个构造方法,如下所示:
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
$self->_determined_init();
return $self;
}
对 $class->SUPER::new()
的调用会调用 LWP::RobotUA::new()
,因为这是 @INC
中的第一个 class。反过来,调用 LWP::UserAgent::new()
- 这样初始化就完成了。然后我们只需要调用 _determined_init()
来初始化另一个 superclass.
它似乎在我的(非常基本的)测试中起作用。但我仍然对多重继承持怀疑态度:-)
更新 2: 是的。
如果不更改 LWP::UA::Determined and/or LWP::RobotUA.
,那将无法工作但是这些更改可以通过猴子修补来应用。底部给出了解决方案。
问题
LWP::UA::Determined 包装了 ->simple_request
,也就是说它提供了一个 ->simple_request
调用 ->SUPER::simple_request
(即 ->LWP::UA::simple_request
)
LWP::RobotUA 包装了 ->simple_request
,也就是说它提供了一个 ->simple_request
调用 ->SUPER::simple_request
(即 ->LWP::UA::simple_request
)
当您调用 ->request
时,您最终会调用 ->simple_request
。根据 @ISA
中模块的顺序,这将调用 LWP::UA::Determined::simple_request
(然后 LWP::UA::simple_request
)或 LWP::RobotUA::simple_request
(然后 LWP::UA::simple_request
)。
它不会同时调用 LWP::UA::Determined::simple_request
和 LWP::RobotUA::simple_request
。如果它这样做了,它就不会工作,因为那最终会调用 LWP::UA::simple_request
两次。
->new
存在完全相同的问题。
演示
use feature qw( say );
{ package Base;
sub method { say __PACKAGE__; } }
{ package SubA; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->SUPER::method(); } }
{ package SubB; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->SUPER::method(); } }
{ package SubAB; our @ISA = qw( SubA SubB ); }
SubAB->method(); # SubA Base
解决方案
LWP::UA::Determined 和 LWP::RobotUA 需要使用 next::method
而不是 SUPER
(或者它们需要完全不同的设计),这样才有机会工作。
演示
use feature qw( say );
{ package Base;
sub method { say __PACKAGE__; } }
{ package SubA; use mro 'c3'; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->next::method(); } }
{ package SubB; use mro 'c3'; our @ISA = qw( Base );
sub method { say __PACKAGE__; $_[0]->next::method(); } }
{ package SubAB; use mro 'c3'; our @ISA = qw( SubA SubB ); }
SubAB->method(); # SubA SubB Base
解决方法
所有的希望都没有破灭!我们可以猴子修补 LWP::UA::Determined 和 LWP::RobotUA 以获得所需的行为!
只需将此添加到您的程序中:
use LWP::RobotUA qw( );
BEGIN {
package LWP::RobotUA::Inserted;
use mro 'c3';
our @ISA = @LWP::RobotUA::ISA;
sub new { return $_[0]->next::method(); }
sub simple_request { return $_[0]->next::method(); }
sub agent { return $_[0]->next::method(); }
@LWP::RobotUA::ISA = __PACKAGE__;
}
use LWP::UserAgent::Determined qw( );
BEGIN {
package LWP::UserAgent::Determined::Inserted;
use mro 'c3';
our @ISA = @LWP::UserAgent::Determined::ISA;
sub new { return $_[0]->next::method(); }
sub simple_request { return $_[0]->next::method(); }
@LWP::UserAgent::Determined::ISA = __PACKAGE__;
}
您还需要 use mro 'c3';
在您自己的 class 中(或仔细选择 @ISA
的顺序)才能使 ->agent
正常工作。
演示
# Insert code from first demo here.
{ package SubA::Inserted; use mro 'c3'; our @ISA = @SubA::ISA;
sub method { return $_[0]->next::method(); }
@SubA::ISA = __PACKAGE__; }
{ package SubB::Inserted; use mro 'c3'; our @ISA = @SubB::ISA;
sub method { return $_[0]->next::method(); }
@SubB::ISA = __PACKAGE__; }
SubAB->method(); # SubA SubB Base