Perl/Moose - 如何动态选择特定方法的实现?

6
我编写了一个基于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

但是这段代码至少存在两个问题:

  1. 我需要在_build_fetcher中保留所有已知实现的列表。我更希望一种方式,让代码自动从命名空间Document::Fetcher::下加载的每个模块/类中选择。或者也许有一种更好的方法来“注册”这些插件?

  2. 目前整个代码看起来有点臃肿。我相信人们之前已经编写过这种插件系统。MooseX中没有提供所需行为的内容吗?

1个回答

7
你需要的是一个工厂,具体来说是一个抽象工厂。你的工厂类的构造函数将根据其参数确定要返回哪个实现。
# Returns Document::Fetcher::ImplA or Document::Fetcher::ImplB or ...
my $fetcher = Document::Fetcher::Factory->new( url => $url );

_build_fetcher 中的逻辑应该放到 Document::Fetcher::Factory->new 中。这样可以将 Fetchers 与 Documents 分离开来。Fetchers 可以自己知道需要哪个实现,而不是 Document 知道如何找到它所需的 Fetcher 实现。

如果您的重点是允许用户添加新的 Fetchers 而无需更改 Factory,则具有 Fetcher 角色能够通知 Factory 它是否能够处理它的基本模式是很好的。但缺点是 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;
}

你可以预加载一些常见的Fetchers,可能是文档相关的,如果你想既要有蛋糕又想吃掉它。

我甚至没有考虑过像GRASP这样的原则。不知何故,我用Moose做的方式似乎是“一种好的方式”。现在你提到了它们,使用抽象工厂当然是很有道理的。我仍然不确定如何注册每个角色。这不需要某种单例类吗?现在我正在使用一种有点hacky的解决方案:检查%Document :: Fetcher :: - Sebastian Stumpf
1
@SebastianStumpf,你不必因为Moose的人对类数据有哲学上的偏见而使事情变得复杂,也不必使用全局变量。正常的封装仍然有效。 - Schwern
最终,我通过向工厂的元类添加一个特征来更加"Mooseish"地解决了这个问题,该特征包含一个名为fetchersArrayRef[Str]属性。因此,我只需调用__PACKAGE__->meta->fetchers->add即可。 :-) - Sebastian Stumpf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接