使用Moo::Role
,我发现循环导入正在默默地阻止执行我的方法的before
修饰符。
我在Moo::Role
有一个MyRole.pm
:
package MyRole;
use Moo::Role;
use MyB;
requires 'the_method';
before the_method => sub { die 'This has been correctly executed'; };
1;
...... MyA.pm
的消费者:
package MyA;
use Moo;
with ( 'MyRole' );
sub the_method { die; }
1;
..和另一个在MyB.pm
:
package MyB;
use Moo;
with ( 'MyRole' );
sub the_method { die 'The code should have died before this point'; }
1;
当我运行这个script.pl
时:
#!/usr/bin/env perl
package main;
use MyA;
use MyB;
MyB->new()->the_method();
...我得到The code should have died before this point at MyB.pm line 4.
,但期望看到This has been correctly executed at MyRole.pm line 5
。
我认为这个问题是由循环进口引起的。如果我在use
中切换script.pl
语句的顺序,或者如果我将use MyB;
中的MyRole.pm
更改为require
中的the_method
,它就会消失。
这种行为有望吗?如果是这样,在无法避免循环进口的情况下处理它的最佳方法是什么?
我可以解决这个问题,但是无意中触发它会感到非常容易(特别是因为它会导致before
函数,它通常包含检查代码,以便默默跳过)。
(我正在使用Moo版本2.003004。显然use MyB;
中的MyRole.pm
在这里是多余的,但只有在我简化了这个repro示例的代码之后。)
循环导入可能会变得相当棘手,但行为一致。关键点是:
use Some::Module
表现得像BEGIN { require Some::Module; Some::Module->import }
BEGIN
块。require
。当再次需要时,忽略require
。知道了,我们可以将您的四个文件合并为一个文件,其中包含BEGIN块中的require
d文件。
让我们从您的主文件开始:
use MyA;
use MyB;
MyB->new()->the_method();
我们可以将use
转换为BEGIN { require ... }
并包含MyA
内容。为清楚起见,我将忽略对->import
和MyA
的任何MyB
调用,因为它们在这种情况下不相关。
BEGIN { # use MyA;
package MyA;
use Moo;
with ( 'MyRole' );
sub the_method { die; }
}
BEGIN { # use MyB;
require MyB;
}
MyB->new()->the_method();
with('MyRole')
也做了require MyRole
,我们可以明确说明:
...
require MyRole;
with( 'MyRole ');
那么让我们扩展一下:
BEGIN { # use MyA;
package MyA;
use Moo;
{ # require MyRole;
package MyRole;
use Moo::Role;
use MyB;
requires 'the_method';
before the_method => sub { die 'This has been correctly executed'; };
}
with ( 'MyRole' );
sub the_method { die; }
}
BEGIN { # use MyB;
require MyB;
}
MyB->new()->the_method();
然后我们可以扩展use MyB
,也将MyB的with('MyRole')
扩展为require
:
BEGIN { # use MyA;
package MyA;
use Moo;
{ # require MyRole;
package MyRole;
use Moo::Role;
BEGIN { # use MyB;
package MyB;
use Moo;
require MyRole;
with ( 'MyRole' );
sub the_method { die 'The code should have died before this point'; }
}
requires 'the_method';
before the_method => sub { die 'This has been correctly executed'; };
}
with ( 'MyRole' );
sub the_method { die; }
}
BEGIN { # use MyB;
require MyB;
}
MyB->new()->the_method();
在MyB
,我们有一个require MyRole
,但该模块已被要求。因此,这没有做任何事情。在执行期间,MyRole
只包含以下内容:
package MyRole;
use Moo::Role;
所以角色是空的。 requires 'the_method'; before the_method => sub { ... }
尚未编制。
因此,MyB
组成一个空角色,这不会影响the_method
。
如何避免这种情况?在这些情况下避免使用use
通常很有帮助,因为在初始化当前模块之前中断解析。这导致不直观的行为。
当use
模块只是类并且不影响源代码的解析方式时(例如通过导入子例程),您通常可以将需求推迟到运行时。不仅是执行顶级代码的模块的运行时间,还包括主应用程序的运行时。这意味着将require
粘贴到需要使用导入类的子例程中。由于require
仍然有一些开销,即使已经导入了所需的模块,你可以像state $require_once = require Some::Module
一样保护需求。这样,require就没有运行时开销。
通常:您可以通过在模块的顶级代码中进行尽可能少的初始化来避免许多问题。更喜欢懒惰并推迟初始化。另一方面,这种懒惰也会使您的系统更具动态性和可预测性:很难说出已经发生了什么初始化。
更一般地说,要认真思考你的设计。为什么需要这种循环依赖?您应该决定坚持使用高级代码依赖于低级代码的分层架构,或者使用依赖性反转,其中低级代码依赖于高级接口。混合两者将导致可怕的纠结混乱(展览A:这个问题)。
我确实理解一些数据模型必然具有共同递归类。在这种情况下,通过将相互依赖的类放在单个文件中来手动整理订单可能是最清楚的。