views:

791

answers:

5

Is there a simpler or better (=>easier to maintain) way to use Perl and Moose to instantiate classes based on incoming data?

The following code is a stripped down sample from a project I'm working on.

package FooBar;
use Moose;
has 'SUBCLASS' =>('isa'=>'Str',required=>'1',is=>'ro');
has 'MSG' =>('isa'=>'Str',required=>'1',is=>'ro');

sub BUILD {
      my $self = shift;
      my ($a)=@_;
      bless($self,$a->{SUBCLASS})
}
sub Hi {
   my $self=shift;
   print "Hi, I'm a " . ref($self)  ." and I say [". $self->MSG()."]\n";
}

package Foo;
use Moose;
extends ("FooBar");

package Bar;
use Moose;
extends ("FooBar");

package main;
use strict;
use warnings;

for my $line (<DATA>) {
   my ($case,$msg)=split(/[\n\r,]\s*/,$line);
   FooBar->new(SUBCLASS=>$case,MSG=>$msg)->Hi();
}

__DATA__
Foo, First Case
Bar, Second Case

EDIT: It just struck me that this is pretty much what happens when you call the DBI. Depending on the parameters you pass, it will use entirely different code while maintaining a (mostly) consistent interface

+6  A: 

You could simply do:

$case->new( MSG => $msg )->Hi();

If that is easier or better is up to you to decide.

innaM
using a variable as the class to be instantiated **$case->new(..)** in that way feels very strange. But yes, extremely compact, when compared to my code (where I spell it out **bless( {} ,$case)** Thanks for the tip!
lexu
why is that strange? a class name in Perl is just a string. Foo->blah is exactly the same as "Foo"->blah.
nothingmuch
If there's no logic in the factory to choose the correct subclass to instantiate then its not much of a factory and this is by a wide margin the best solution.
Schwern
My original code (too long to post, so I stripped it down to the sample above) does a DB query and uses some logic do derive the class that needs to be instantiated. I want to encapsulate that in a factory.
lexu
+3  A: 

Well, the object is already created when BUILD is called, so I would say

sub BUILD {
      my $self = shift;
      return bless $self, $self->SUBCLASS;
}

You may always wish to switch from an inheritance based model to a role based model where you create the object you want (rather than passing the class into the factory class), then apply the common role.

Chas. Owens
Where would you send me to learn/understand the transition of " inheritance based" to "role based"?
lexu
The Moose Manual on Roles would be a start http://search.cpan.org/perldoc/Moose::Manual::Roles
Sinan Ünür
@Sinan Ünür: thanks for that link!
lexu
+9  A: 

Ick. Stevan has a very compelling argument that new should always only return an instance of Class. Anything else is confusing to new people learning the system.

You might wanna take a look at MooseX::AbstractFactory. If that won't work for you then:

package FooBar;
use Moose;

has [qw(SUBCLASS MSG)] => ( is => 'ro', required => 1);

sub create_instance {
    return $self->package->new(message => $self->msg);
}

package FooBar::Object;
use Moose;

has msg => ( is => 'ro', required => 1);

sub Hi {
   my $self = shift;
   print "Hi, I'm a " . ref($self)  ." and I say [". $self->MSG()."]\n";
}

package Foo;
use Moose;
extends qw(FooBar::Object);

package Bar;
use Moose;
extends qw(FooBar::Object);


package main;
or my $line (<DATA>) {
   my ($case,$msg)=split(/[\n\r,]\s*/,$line);
   FooBar->new(SUBCLASS=>$case,MSG=>$msg)->create_instance->Hi
}

__DATA__
Foo, First Case
Bar, Second Case

Of course there are many other ways to implement this same concept in Moose. Without knowing the specifics of your domain problem it's hard to tell that something like MooseX::Traits wouldn't be better:

package Foo;
use Moose;
with qw(MooseX::Traits);

package Bar;
use Moose;
with qw(MooseX::Traits);

package Messaging;
use Moose::Role;

has msg => ( is => 'ro', required => 1);

sub Hi {
   my $self = shift;
   print "Hi, I'm a " . ref($self)  ." and I say [". $self->MSG()."]\n";
}

package main;
use strict;
Foo->new_with_traits(traits => ['Messaging'], msg => 'First Case')->Hi;

This is roughly what the other poster meant about using a Role based solution.

perigrin
thanks, will take me some time to grok :-)
lexu
**has [qw(SUBCLASS MSG)] => ( is => 'ro', required => 1);** neat trick .. but "unintuitive" for anyone not familiar with perl ..
lexu
No it's not. It is part of the Moose API and has nothing to do with Perl. (If you wanted to be unintuitive, you would write "has $_ => (...) for qw/SUBCLASS MSG/". But of course, everyone knows what this does too.)
jrockway
I think it's safe to assume anybody who's reading a question about Moose either is familiar with Perl or wishes to be. That syntax is documented in Moose.pm and they're free to ask about it.
perigrin
+5  A: 

Just a note on some of the answers:

Calling bless in BUILD, or anywhere outside of the MOP internals, is always unacceptable. (If you must rebless, there is Class::MOP::Class->rebless_instance!)

I second the advice on not allowing new to return anything other than an instance of __PACKAGE__. If you want a method that creates an instance of something, call it something else. Example:

class Message {
   method new_from_string(Str $msg){
       my ($foo, $bar, $baz) = ($msg =~ /<...>/); # blah blah blah
       my $class = "Message::${foo}::$baz";
       Class::MOP::load_class($class);
       return $class->new( bar => $msg );
   }
}

Then, when you want to create a literal message:

Message->new( whatever => 'you want' );

When you want to parse a string and return the correct message subclass:

Message->new_from_string( 'OH::Hello!' );

Finally, if it doesn't make sense to be able to create an instance of Message, then it should not be a class. It should be a role.

You can handle building with some other object, of course. Just make sure this other object is responsible only for understanding the string format, for example, and not message internals:

class MessageString {
    has 'string' => ( initarg => 'string', reader => 'message_as_string' );

    method new_from_string(ClassName $class: Str $string) {
        return $class->new( string => $string );
    }

    method as_message_object {
        # <parse>
        return Message::Type->new( params => 'go here', ... );
    }
}

role Message { ... }
class Message::Type with Message { ... }

Now you are no longer concerned with having some "superclass" responsible for building "subclasses", which I think is better design. (Remember, MessageString has no special power over the classes that do "Message". That is the key here; it is only responsible for understanding stringified messages.)

Anyway, now you just:

my $data =  <>; # Yup, I called it $data.  Sorry, Andy Lester.
my $parsed = MessageString->new_from_string( $data );
my $message = $parsed->as_message_object;
$message->interact_with

(You know "MVC"? This is similar.)

jrockway
+2  A: 
nothingmuch