我已经编写了一个简单的基于Moose的类Document。此类具有两个属性:namehomepage

该类还需要提供一种称为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的类,它为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

但是此代码至少存在两个问题:
  • 我需要在_build_fetcher中保留所有已知实现的列表。我希望代码可以自动从 namespace Document::Fetcher::下的每个已加载模块/类中进行选择。还是有更好的方法来“注册”此类插件?
  • 目前,整个代码显得有些肿。我敢肯定,人们以前已经写过这种插件系统。 MooseX中没有提供所需行为的东西吗?
  • 最佳答案

    您正在寻找的是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中。这会将提取程序与您的文档分开。 Fetcher可以自己执行此操作,而不是Document知道如何找出所需的Fetcher实现。

    如果您的首要任务是允许人们添加新的Fetcher而不必更改工厂,则让Fetcher角色能够通知Factory是否能够处理的基本模式是好的。不利的一面是,Fetcher::Factory无法知道多个Fetcher对于一个给定的URL可能是有效的,并且一个可能比另一个更好。

    为了避免Fetcher::Factory中有大量的Fetcher实现硬编码,请在加载Fetcher::Factory时将每个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;
    }
    

    如果您想吃蛋糕,也可以预装一堆常见的Fetchers,可能是Document。

    10-05 21:08