2012-06-08 15 views
6

He escrito una clase simple Moose llamada Document. Esta clase tiene dos atributos: name y homepage.Perl/Moose: ¿cómo puedo elegir dinámicamente una implementación específica de un método?

La clase también necesita proporcionar un método llamado do_something() que recupera y devuelve texto de diferentes fuentes (como un sitio web o bases de datos diferentes) basado en el atributo homepage.

Dado que habrá muchas implementaciones totalmente diferentes para do_something(), me gustaría tenerlas en diferentes paquetes/clases y cada una de estas clases debería saber si es responsable del atributo homepage o si no lo está .

Mi enfoque implica hasta ahora dos funciones:

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'; 

una clase llamada Document::Fetcher que proporciona una implmenentation predeterminado para do_something() y métodos de uso común (como una solicitud GET HTTP):

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; 
    # ... 
} 

Y las implementaciones específicas que determinan su responsabilidad a través de un método llamado 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#; } 

Mi clase Document se ve así:

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); 
} 

En este momento esto funciona como debería. Si llamo el siguiente código:

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

consigo la salida esperada:

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

Pero hay al menos dos problemas con este código:

  1. necesito mantener una lista de todas las implementaciones conocidas en _build_fetcher. Prefiero una forma en la que el código elija automáticamente de cada módulo/clase cargado debajo del espacio de nombres Document::Fetcher::. ¿O tal vez hay una mejor manera de "registrar" este tipo de complementos?

  2. Por el momento todo el código parece un poco demasiado hinchado. Estoy seguro de que la gente ha escrito este tipo de sistema de complementos antes. ¿No hay algo en MooseX que proporciona el comportamiento deseado?

Respuesta

7

Lo que estás buscando es un Factory, específicamente un Abstract Factory. El constructor de su clase Factory determinará qué implementación devolver en función de sus argumentos.

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

La lógica en _build_fetcher entraría en Document::Fetcher::Factory->new. Esto separa los Recolectores de tus Documentos. En lugar de que Document sepa cómo determinar qué implementación de Fetcher necesita, los Fetchers pueden hacerlo por sí mismos.

Su patrón básico de tener la función Fetcher capaz de informar a la fábrica si es capaz de manejarlo es bueno si su prioridad es permitir que las personas agreguen nuevas capturas sin tener que alterar la fábrica. En el lado negativo, Fetcher :: Factory no puede saber que múltiples Fetchers podrían ser válidos para una URL determinada y que uno podría ser mejor que el otro.

Para evitar tener una gran lista de implementaciones de Fetcher codificadas en su Fetcher :: Factory, haga que cada función de Fetcher se registre con Fetcher :: Factory cuando se cargue.

my %Registered_Classes; 

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

    $Registered_Classes{$registeree}++; 

    return; 
} 

sub registered_classes { 
    return \%Registered_Classes; 
} 

Puede tener algo, probablemente documento, pre-cargar un montón de Fetchers comunes si desea que su pastel y comérselo también.

+0

Ni siquiera he pensado en principios como GRASP. De alguna manera, la forma en que lo hice parecía ser "una buena manera" con Moose. Ahora que los has mencionado, por supuesto tiene sentido para usar una fábrica abstracta. Todavía no estoy seguro de cómo registrar cada función. ¿Esto no requeriría algún tipo de clase de Singleton? En este momento estoy usando una solución algo hacky: examinando '% Document :: Fetcher ::'. –

+1

@SebastianStumpf No tiene que complicarse las cosas solo porque la gente de Moose tiene un resentimiento filosófico contra los datos de clase, ni tiene que alcanzar las variables globales. La encapsulación normal todavía funciona. – Schwern

+0

Resolví esto al final un poco más "Mooseish" al agregar un rasgo a la clase meta de la factoría que tiene un atributo 'ArrayRef [Str]' llamado 'fetchers'. Así que puedo simplemente llamar a '__PACKAGE __-> meta-> fetchers-> add'. :-) –

Cuestiones relacionadas