6

使用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如果我将语句的顺序切换为 inscript.pl或将use MyB;in更改MyRole.pmrequirewithin ,它就会消失the_method

这种行为是预期的吗?如果是这样,在无法避免循环导入的情况下,最好的处理方法是什么?

我可以解决这个问题,但感觉很容易意外触发(特别是因为它会导致before通常包含检查代码的函数被静默跳过)。

(我使用的是 Moo 版本 2.003004。显然这里的use MyB;inMyRole.pm是多余的,但只有在我简化了这个 repro 示例的代码之后。)

4

1 回答 1

3

循环导入可能会变得相当棘手,但行为一致。关键点是:

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

知道了这一点,我们可以将您的四个文件组合成一个文件,其中包含requireBEGIN 块中的 d 文件。

让我们从您的主文件开始:

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

我们可以将其转换useBEGIN { require ... }并包含MyA内容。为清楚起见,我将忽略任何->import调用MyAMyB因为它们在这种情况下不相关。

BEGIN { # use MyA;
  package MyA;
  use Moo;
  with ( 'MyRole' );
  sub the_method { die; }
}
BEGIN { # use MyB;
  require MyB;
}
MyB->new()->the_method();

with('MyRole')也做了 a ,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')为 a 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.


如何避免这种情况?在这些情况下避免 a 通常很有帮助,use因为这会在当前模块初始化之前中断解析。这会导致不直观的行为。

当你的模块use只是类并且不影响你的源代码的解析方式(例如通过导入子例程)时,你通常可以推迟运行时间的要求。不仅是执行顶级代码的模块的运行时间,还包括主应用程序的运行时间。这意味着将您require插入需要使用导入类的子例程中。由于require即使已经导入了所需的模块, a 仍然有一些开销,因此您可以像state $require_once = require Some::Module. 这样,require 就没有运行时开销。

一般来说:您可以通过在模块的顶级代码中进行尽可能少的初始化来避免许多问题。更喜欢懒惰并推迟初始化。另一方面,这种惰性也会使您的系统更加动态且难以预测:很难判断已经发生了什么初始化。

更一般地说,认真考虑您的设计。为什么需要这种循环依赖?您应该决定要么坚持高级代码依赖于低级代码的分层架构,要么使用低级代码依赖于高级接口的依赖倒置。将两者混合会导致可怕的混乱(图表A:这个问题)。

我确实了解某些数据模型必然具有共同递归类。在这种情况下,通过将相互依赖的类放在一个文件中来手动排序是最清楚的。

于 2017-12-20T11:07:12.483 回答