24 days of Perl code from RJBS! Feed

Object Marginalia

Mixin::ExtraFields - 2009-12-22

Scribbling Notes on an Object

Mixin::ExtraFields is hard to summarize, but here's a whack at it: it's a system for storing extra data related to an object and creating methods for accessing and altering that state.

Here's a really simple example to start with. We have an object that's a blessed hashref and we want to have a bucket for arbitrary state:


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

 

package Holiday;

use Mixin::ExtraFields -fields => {
  moniker => 'tradition',
  driver => 'HashGuts',
};

sub new { ... }

sub id { ... }

 

Now we've got this bucket for extra values in our Holiday objects which we can use in a few ways, like:


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

 

my $holiday = Holiday->new('Christmas');

$holiday->set_tradition(colors => [ qw(red green) ]);
$holiday->set_tradition(music => 'Unbearable');
$holiday->set_tradition(icon => 'Santa Claus');

# ...

die "nothing to eat!" unless $holiday->exists_tradition('food');

for my $thing ($holiday->get_all_tradition_names) {
  print stringf "The traditional %s for %i is %r.\n",
    $thing,
    $holiday->id,
    $holiday->get_tradition($thing);
}

 

If we use Data::Dumper on our object, we might see something like this:


1: 
2: 
3: 
4: 
5: 
6: 

 

$VAR1 = bless( {
  id => 'Christmas',
  'Mixin::ExtraFields::Driver::HashGuts=HASH(0x100864e18)@0' => {
    foo => 10,
  },
}, 'Holiday');

 

That horrible key is generated as a place in our object to put a reference to its associated data. That's only one reference, though. We keep track of data for that object externally, too. That means if we create another object with the same id, it will have the same traditions.

This means we can also have objects share their extra fields:


1: 
2: 
3: 
4: 
5: 

 

package Holiday;

use Mixin::ExtraFields
  -fields => { moniker => 'tradition', driver => 'HashGuts' },
  -fields => { moniker => 'month_info', driver => 'HashGuts', id => 'month' };

 

Then we can say:


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

 

my $christmas = Holiday->new({ id => 'Christmas', month => 'Dec' });
my $kwanzaa = Holiday->new({ id => 'Kwanzaa', month => 'Dec' });

$christmas->set_tradition(color => 'green');
$christmas->get_tradition('color'); # ==> green
$kwanzaa->get_tradition('color'); # ==> undef

$christmas->set_month_info(length => 31);
$christmas->get_month_info('length'); # ==> 31
$kwanzaa->get_month_info('length'); # ==> 31

 

This can be a quick way to add state to a class you don't control for the sake of debugging:


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

 

use Weird::Object;
use Mixin::ExtraFields
  { into => 'Weird::Object' }, # add methods to that class, not this one
  -fields => {
    -prefix => '__', # make our methods very private
    driver => 'HashGuts',
    id => undef, # they have no id-like method; use the refaddr
    moniker => 'debug',
  };

my $object = Weird::Object->new( ... );

$object->__set_debug(status => 'brand new');

 

Alternate Sets of Methods

It's easy to provide methods other than the get_extra and set_extra (and the half dozen related methods) that we saw above. For example, you could use Mixin::ExtraFields::Param instead and get a method like CGI.pm's param:


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

 

package Holiday;
use Mixin::ExtraFields::Param -fields => {
  moniker => 'tradition',
  driver => 'HashGuts',
};

# ...

$holiday->tradition(
  color => [ qw(red green) ],
  food => 'sugarplum',
  tree => 'artificial',
);

$holiday->tradition('food'); # ==> sugarplum

 

Or, my favorite, Mixin::ExtraFields::Hive to get a Data::Hive-based hierarchical-looking set of methods. The hive makes it look like you have a deep storage structure, but flattens everything out to key/value pairs for storage. It lets us do tricks like this:


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

 

package Holiday;
use Mixin::ExtraFields::Hive -hive => {
  moniker => 'tradition',
  driver => 'HashGuts',
};

# ...

$holiday->tradition->floral->tree->decoration->SET('tinsel');

$holiday->tradition->mood->malice->DELETE;

if (my $phrase = $holiday->tradition->exclamation->GET) {
  say $phrase;
}

 

The fact that Data::Hive takes this apparently structured data and stores it in a flat table is extremely useful, because it keeps to a known quantity the number of methods that alternate drivers have to provide.

Alternate Storage Drivers

Alternate storage is why we had to keep saying we wanted the HashGuts driver, above. If you wanted, you could write a driver that stored all attributes only in some otherwise inaccessible hashref. You could store attributes by calling another set of methods on the object being altered. There are lots of possibilites, but the one I've used the most is storing the table of extra values in a database table.

We can decorate objects in our program with these extra fields, storing any set values in a database and retrieving them back from the database. So we can write a class like this:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 

 

package Holiday;
use Calendar::Schema;

use Mixin::ExtraFields::Hive -hive => {
  moniker => 'tradition',
  driver => {
    class => 'DBIC',
    schema => sub { Calendar::Schema->connect(...) },
    rs_moniker => 'HolidayTradition',
  },
};

sub new {
  my ($class, $holiday) = @_;
  bless { id => $holiday } => $class;
}

sub id {
  my ($self) = @_;
  return $self->{id};
}

 

It's about the simplest object you could get, with just one attribute stored in a plain old hash, but then it has this "hive" mixed in that persists things in your DBIx::Class database schema. You can turn your little holiday objects into flyweights for accessing (apparently) structured data like:


1: 
2: 

 

printf "Tonight we will eat %s!",
  Holiday->new('Christmas')->tradition->food->dinner->entree->GET;

 

...and we'll pull that out of the database from the initial load of all our favorite holiday traditions. (To keep things easy for you, the DBIC driver can actually set up the DBIC resultsource for you, so your HolidayTradition source above, need only be two or three lines of code.

Sometimes you might just want to stick your extra data into an object's guts, and sometimes you might just want to use a full database layer to map your whole object to SQL, and sometimes you might want a full meta-programming layer for adding attributes to your classes. Other times, though, you need a quick way to add data to objects and share it correctly without having to think about it too much. That's exactly when I start mixing this library in.

See Also