2012-06-08 39 views
6

我已經寫了叫Document簡單Moose基於類。這門課有兩個屬性:namehomepagePerl/Moose - 如何動態選擇某個方法的特定實現?

該類還需要提供一種名爲do_something()的方法,該方法根據homepage屬性檢索並返回來自不同來源(如網站或不同數據庫)的文本。

由於do_something()會有很多完全不同的實現,所以我希望將它們放在不同的包/類中,並且這些類中的每一個都應該知道它是否對homepage屬性負責,或者如果它不是。

我的做法,到目前爲止涉及到兩個角色:

package Role::Fetcher; 
use Moose::Role; 
requires 'do_something'; 
has url => (
    is => 'ro', 
    isa => 'Str' 
); 

package Role::Implementation; 
use Moose::Role; 
with 'Role::Fetcher'; 
requires 'responsible'; 

稱爲Document::Fetcher一類,它提供一個默認的implmenentation爲do_something()和常用的方法(如HTTP GET請求):

package Document::Fetcher; 
use Moose; 
use LWP::UserAgent; 
with 'Role::Fetcher'; 

has ua => (
    is => 'ro', 
    isa => 'Object', 
    required => 1, 
    default => sub { LWP::UserAgent->new } 
); 

sub do_something {'called from default implementation'} 
sub get { 
    my $r = shift->ua->get(shift); 
    return $r->content if $r->is_success; 
    # ... 
} 

並決定通過一個方法爲己任的具體實現所謂responsible()

package Document::Fetcher::ImplA; 
use Moose; 
extends 'Document::Fetcher'; 
with 'Role::Implementation'; 

sub do_something {'called from implementation A'} 
sub responsible { return 1 if shift->url =~ m#foo#; } 

package Document::Fetcher::ImplB; 
use Moose; 
extends 'Document::Fetcher'; 
with 'Role::Implementation'; 

sub do_something {'called from implementation B'} 
sub responsible { return 1 if shift->url =~ m#bar#; } 

Document類看起來是這樣的:

package Document; 
use Moose; 

has [qw/name homepage/] => (
    is => 'rw', 
    isa => 'Str' 
); 

has fetcher => (
    is => 'ro', 
    isa => 'Document::Fetcher', 
    required => 1, 
    lazy => 1, 
    builder => '_build_fetcher', 
    handles => [qw/do_something/] 
); 

sub _build_fetcher { 
    my $self = shift; 
    my @implementations = qw/ImplA ImplB/; 

    foreach my $i (@implementations) { 
     my $fetcher = "Document::Fetcher::$i"->new(url => $self->homepage); 
     return $fetcher if $fetcher->responsible(); 
    } 

    return Document::Fetcher->new(url => $self->homepage); 
} 

現在這個工作,因爲它應該。如果我把下面的代碼:

foreach my $i (qw/foo bar baz/) { 
    my $doc = Document->new(name => $i, homepage => "http://$i.tld/"); 
    say $doc->name . ": " . $doc->do_something; 
} 

我得到預期的輸出:

foo: called from implementation A 
bar: called from implementation B 
baz: called from default implementation 

但至少有兩個問題與此代碼:

  1. 我需要保持_build_fetcher中所有已知實現的列表。我喜歡的方式,其中的代碼會自動從每加載模塊/類選擇的命名空間Document::Fetcher::下方。或者,有更好的方法來「註冊」這些插件?

  2. 目前整個代碼看起來有點過於臃腫。我相信人們以前寫過這種插件系統。 MooseX中沒有提供所需行爲的東西嗎?

回答

7

什麼你要找的是Factory,具體的Abstract Factory。 Factory類的構造函數將根據參數確定要返回的實現。

# Returns Document::Fetcher::ImplA or Document::Fetcher::ImplB or ... 
my $fetcher = Document::Fetcher::Factory->new(url => $url); 

_build_fetcher邏輯將進入Document::Fetcher::Factory->new。這將Fetchers從您的文檔中分離出來。 Document不知道如何知道它需要哪個Fetcher實現,Fetchers可以自己做。

如果您的優先考慮是允許人們添加新的Fetchers而無需更改工廠,那麼讓您具有Fetcher角色的基本模式能夠告知工廠是否能夠處理該問題。另一方面,Fetcher :: Factory無法知道多個Fetchers對於給定的URL可能是有效的,並且這可能比另一個更好。

爲了避免在您的Fetcher :: Factory中有一個大的Fetcher實現硬編碼列表,每個Fetcher角色在加載時都會向Fetcher :: Factory註冊。

my %Registered_Classes; 

sub register_class { 
    my $class = shift; 
    my $registeree = shift; 

    $Registered_Classes{$registeree}++; 

    return; 
} 

sub registered_classes { 
    return \%Registered_Classes; 
} 

如果你想要你的蛋糕和食物,你可以有一些東西,可能是Document,預加載一堆常見的Fetchers。

+0

我甚至沒有想過像GRASP這樣的原理。不知怎的,我做到這一點似乎是與穆斯「好方法」。現在您已經提到過,使用抽象工廠當然非常合理。我仍然不確定如何註冊每個角色。這不需要某種Singleton類嗎?現在我正在使用一個有點冒險的解決方案:檢查'%Document :: Fetcher ::'。 –

+1

@SebastianStumpf你不必因爲穆斯的人對班級數據有哲學怨恨而使事情變得複雜,也不必爲全局變量伸手。正常封裝仍然有效。 – Schwern

+0

我通過在factorie的元類中添加一個特徵來解決這個問題,該特徵類包含一個名爲'fetchers'的'ArrayRef [Str]'屬性。所以我可以調用'__PACKAGE __-> meta-> fetchers-> add'。 :-) –

相關問題