2017-12-18 192 views
6

使用Moo::Role,我发现循环导入默默阻止了我的方法的修改器before的执行。如何处理:由于循环导入,Moo :: Role的`before`修饰符默默跳过?

我有一个Moo::RoleMyRole.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

我觉得这个问题是由循环导入造成的。如果我将script.pl中的use语句的顺序切换,或者如果我将中的的use MyB;更改为require,则它将消失。

预计这种行为?如果是这样,在无法避免循环进口的情况下处理该问题的最佳方法是什么?

我可以解决这个问题,但感觉非常容易无意中触发(特别是因为它导致before功能,通常包含检查代码,被默默跳过)。

(我使用的是武版本2.003004。很明显,在MyRole.pmuse MyB;是多余的,但在这里我已经简化了这个摄制例子中的代码之后。)

回答

3

循环进口可以得到相当棘手,但行为一致。关键点是:

  1. use Some::Module行为就像BEGIN { require Some::Module; Some::Module->import }
  2. 当一个模块被加载时,它被编译并执行。 BEGIN块在解析周围代码时执行。
  3. 每个模块只有require'd一次。当再次需要时,那require忽略

知道了,我们可以将您的四个文件合并到一个单独的文件中,该文件在BEGIN块中包含require d文件。

让我们先从你的主文件:

use MyA; 
use MyB; 
MyB->new()->the_method(); 

我们可以改变useBEGIN { require ... }和包括MyA内容。为了清楚起见,我将忽略在MyAMyB上的任何->import调用,因为它们在这种情况下不相关。

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粘贴到需要使用导入类的子例程中。由于即使所需模块已经导入,require仍然存在一些开销,因此您可以保护state $require_once = require Some::Module之类的要求。这样,这个需求就没有运行时间的开销。

一般来说:通过在模块的顶级代码中尽可能少地进行初始化,可以避免许多问题。倾向于懒惰并推迟初始化。另一方面,这种懒惰也会使你的系统变得更加动态和难以预测:很难说出已经发生了什么样的初始化。

更一般地说,想想你的设计。为什么需要这种循环依赖?您应该决定采用分层体系结构,其中高级代码依赖于低级代码,或者在低级代码依赖于高级接口的情况下使用依赖性反转。将两者混合会导致可怕的混乱(展品A:这个问题)。

我明白一些数据模型必须具有共递归类。在这种情况下,通过将相互依赖的类放置在单个文件中来手动分拣订单可能是最清楚的。