eight crazy nights of Perl code from RJBS Feed

Factory Factory Factory Factory

MooseX::ClassCompositor - 2011-12-23

I like building classes!

I've really been happy with doing more and more stuff with roles instead of subclasses. More and more, I've ended up writing a bunch of roles all related to some kind of thing that I want to make, and then I pick the roles I want, when I make the thing. Is that clear as mud? Let me elaborate:

Stevan Little and I wrote HTTP::Throwable, a system for throwing exceptions that will be transformed into HTTP responses. There are a bunch of roles that might show up in these: Redirect, for example, means that the exception is going to need a Location header. BoringText can be brought in to let any of the methods get a very simple, boring text_body method.

If you wanted exceptions to have more data, for communicating detailed error messages, you might write a JSONRPC role that gave them a few more attributes and a json_body method. You might add a ForUser role that tacked on even more data, but only if the error came after authentication. It should be easy to imagine many more such roles.

Now, imagine that you've figured out all the roles you might need for your application's full gamut of exception behavior, and that you might need any possible intersection of those. Just naming each and every combination would be a pain. Then you'd have to remember those names, or at least the rule you used to name them. Then you'd have to generate all that code and put it into files. Then you'd have to make sure all the classes were loaded. What a drag!

Moose is meant to make a lot of this unneeded, because it gives you tools to generate classes easily at runtime without doing a lot of nasty futzing about with globs and stashes – or worse, building up strings and using eval. Moose lets you generate classes with a nice, simple method-based API. Even that is kind of a drag to use, though, and I wanted something better.

After writing a few one-off somethings kinda-better, I brought some of the designs to the table at work, where my colleague Mark Jason Dominus and I hammered together MooseX::ClassCompositor.

Class Compositor?

A MooseX::ClassCompositor is a class factory: it's a thing whose job is to churn out classes. I didn't want to call it MooseX::ClassFactory, though, because there's a pretty strong knee-jerk reaction among many Perl programmers that "factory" means "overcomplicated." In fact, I bet I've lost a few readers in the last few lines alone by admitting that this article is about a class factory.

Really, a class factory is a simple thing, and it's used to make your code simpler. You tell it what kind of class you want, and it builds it for you. First, though, you have to set the factory up with some settings to explain what kind of classes you plan to build. In our case, we want to make HTTP::Throwble-ish classes.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 

 

my $compositor = MooseX::ClassCompositor->new({
  class_basename => 'MyApp::HTTP::Throwable',
  fixed_roles => [ '=HTTP::Throwable' ],
  role_prefixes => {
    '' => 'HTTP::Throwable::Role::',
    '=' => '',
  },
});

my $class = $compositor->class_for( qw( Redirect JSONRPC ForUser ));

$class->throw( ... );

 

We've configured our compositor to always compose the HTTP::Throwable role. If you pass arguments to its class_for method, it will expand them according to the role_prefixes you gave it (using String::RewritePrefix) and then also compose those roles. These generated classes get automatically-generated names, too, but we've specified a starting namespace for them with the class_basename parameter, so we can identify the objects more or less, as needed.

Calls to class_for are memoized, too. That means that if you keep asking for the same class over and over, it will quickly give you the one it built before, so it doesn't need to go through all the work of role summation, glob muckery, and so on.

More Complicated Composition

There are a few more useful things you can do with your compositor. For one, you can compose class metaroles. If you know what this is, you may already be glad to hear you can do it. If you don't know what this is, the use case 99% of the time will be adding the following class_metaroles argument:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 

 

my $compositor = MooseX::ClassCompositor->new({
  class_basename => 'MyApp::HTTP::Throwable',
  fixed_roles => [ '=HTTP::Throwable' ],
  class_metaroles => {
    class => [ 'MooseX::StrictConstructor::Trait::Class' ],
  },
  role_prefixes => {
    '' => 'HTTP::Throwable::Role::',
    '=' => '',
  },
});

 

...and now all your constructed classes have strict constructors. If you get nothing else out of this article, click that link and start using strict constructors everywhere.

You can also use parameterized roles with your compositor:


1: 
2: 
3: 
4: 
5: 

 

my $class = $compositor->class_for(
  'Redirect',
  [ RPC => JSONRPC => { our_ident => 'myApp', default_port => 8080 } ],
  'ForUser',
);

 

Array references in the roles list use the first element as the nonce name for the parameterized role (for memoization) and the other two parameters as the role's short name and parameters.

Sugar!

The way we actually use this is actually with something a bit more like this:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 

 

use Sub::Exporter -setup => [ qw(error) ];

my $COMPOSITOR = MooseX::ClassCompositor->new({
  class_basename => 'MyApp::HTTP::Throwable',
  fixed_roles => [ '=HTTP::Throwable', ... ],
  class_metaroles => { class => [ 'MooseX::StrictConstructor::Trait::Class' ] },
  role_prefixes => {
   '' => 'HTTP::Throwable::Role::',
   '=' => '',
  }
});

sub error { $COMPOSITOR->class_for(@_); }

 

...then, anywhere in our code base where we've used that package and imported error, we can write:


1: 
 

error(qw(ForUser Plumbing Temporary))->throw("We'll be right back!");
 

Because of MooseX::ClassCompositor, we've ended up with much, much less of our program's logic stored in prebuilt classes. Instead, we have been able to break things into reusable roles that we compose as needed, efficiently, just in time for use. It's been a very successful and enjoyable experiment.

See Also