I'm currently working with a lot of legacy code in an environment where the other developers haven't been brought up to speed on things like Moose just yet. I've been tasked with writing some modules in support of this legacy code on a legacy platform - so Moose probably isn't the first choice of tools to reach for.
The module I created is a lightweight wrapper around PDF::API2 that handles a very specific case - adding an image to an existing PDF file. Naturally, Moose is installed on my laptop, so after creating a recent module in older style perl with a hash based object I decided to re-implement it in Moose.
The first comparison I'll look at is object instantiation.
use constant ARGS => qw/pdf_template image new_filename/;
sub new {
my( $class, $arg_ref ) = @_;
my( $pdf_template, $image_file, $new_filename )
= ref $arg_ref eq 'HASH'
? @{ $arg_ref }{ ( ARGS ) }
: ( $arg_ref );
my $self = {};
bless $self, $class;
$self->_clear_saved;
die "Can't read PDF template - $!" unless $pdf_template && -r $pdf_template;
$self->_set_pdf( PDF::API2->open( $pdf_template ) );
$self->add_image( $image_file ) if $image_file;
$self->save( $new_filename ) if $image_file && $new_filename;
return $self;
}
sub _set_pdf {
my( $self, $pdf ) = @_;
die "Invalid PDF object" unless $pdf && ( ref $pdf ) =~ /^PDF::API2/;
$self->{pdf} = $pdf;
$self->_set_page( $self->_load_page );
$self->_set_gfx( $self->_load_gfx );
$self->_set_dimensions( $self->_load_dimensions );
return;
}
There's really just a few things to point out here.
I've designed the object so that it will either accept a single string as an argument, or a hashref containing key => value pairs of the pieces of data used to setup the object; in this case a PDF template filename, an image filename, and a new filename to save a resultant file as. To make this module as easy as possible to use, I have it setup so that if all arguments are passed in the constructor, the object knows enough about what's needed from it and does everything - even saves the new file.
I've included the _set_pdf() method because that one is always called for my definition of a successful object instantiation. As you can see, once it sets the private object member {pdf} to the new PDF::API2 object passed in, it also sets a few other object member data based on the PDF::API2 object.
After setting these in _set_pdf(), the code checks to see if there's an image file and a new filename. We'll get to that part later.
One way to create the analog to the above code using Moose :
use Moose;
use Moose::Util::TypeConstraints;
has 'pdf_template' => (
is => 'rw',
isa => 'Str',
required => 1
);
has 'image_file' => (
is => 'rw',
isa => 'Str'
);
has 'new_filename' => (
is => 'rw',
isa => 'Str'
);
has 'image' => (
is => 'rw',
isa => 'PDF::API2::Resource::XObject::Image::JPEG'
);
subtype 'MyPDF'
=> as 'PDF::API2';
coerce 'MyPDF'
=> from 'Str'
=> via { PDF::API2->open( $_ ) };
has 'pdf' => (
is => 'ro',
isa => 'MyPDF',
coerce => 1,
lazy_build => 1,
);
sub _build_pdf { shift->pdf_template }
sub BUILD {
my $self = shift;
if( my $image = $self->image_file ){
$self->image( $image );
if( my $filename = $self->new_filename ){
$self->save( $filename );
}
}
}
The first item of interest is that I'm letting Moose::Util::TypeConstriants handle the coercion of a string (the PDf filename) to a PDF::API2 object. I'm also setting the pdf attribute to lazy_build. A little more on that later.
Additionally - for the rest of the attributes, most of the work is done for me by Moose - I only need to tell it what attributes my class is going to have, and Moose does all the setting and type checking for me.
Notice there's no new() subroutine - that's because Moose takes care of creating a default constructor, which will take care of parsing the argument(s) to the constructor for us.
Because I am creating a literal translation of the previous object in Moose , I decided to give it the exact same behavior. I originally started adding the extra functionality to the constructor by creating an after() modifier, but that had a couple of problems - the first of which was the fact that I could no longer follow the Moose best practice of making the class immutable .
I also ran into an issue using after() with deep recursion - notice how after setting the pdf attribute I use it (the pdf attribute) again to set the page attribute. This had the fun effect of calling after() again, after accessing the pdf attribute. Oops :)
I then tried around() but that had also didn't let me set the class to be immutable. So I settled on using a BUILD() method.
I originally had a few more calls to other attributes' setup inside my BUILD() subroutine, but Chris Prather suggested I move those pieces to lazy_build attributes , in which advice (and the accompanying documentation) I found great wisdom. The thinking is that the value for these attributes depend on another attribute - so these attributes /must/ be lazy. I've left out those definitions, but I'll get to those in a later post.