2017-12-18 195 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:這個問題)。

我明白一些數據模型必須具有共遞歸類。在這種情況下,通過將相互依賴的類放置在單個文件中來手動分揀訂單可能是最清楚的。