NAME

Class::Driver - Generate driver ("composite") class hierarchies on-the-fly

EXAMPLE

# This is a really long synopsis, but hopefully it will give you an idea...

package MyPackage;

use Class::Driver;
use base q(Class::Driver);

our %drivers;

return 1;

sub new {
  my($class, %args) = @_;
  die "mime_type is required" unless($args{mime_type});
  die "no driver to handle type $args{mime_type}"
      unless($drivers{$args{mime_type}});
  return $class->driver_load($drivers{$args{mime_type}}, %args);
}

sub driver_new {
  my($class, %args) = @_;
  return bless \%args, $class;
}

sub driver_required { 1; }
sub driver_requied_here { 0; }

package MyPackage::avi;

use MyPackage;
use base q(MyPackage);
use Video::Info;

$MyPackage::drivers{'video/x-msvideo'} = 'avi';

return 1;

sub driver { "avi"; }

sub driver_new {
    my($class, %args) = @_;
    die "'file' is a required parameter for $args{mime_type} files"
        unless($args{file});
    $args{info} = Video::Info->new(-file => $args{file})
        or die "Failed to create a Video::Info object for $args{file}";
    return $class->SUPER::driver_new(%args);
}

sub duration {
    my $self = shift;
    return $args{info}->duration;
}

package MyPackage::mp3;
use base q(MyPackage);
use MP3::Info;

$MyPackage::drivers{'audio/mpeg'} = 'mp3';

## (etc...)

package main;

my $foo = MyPackage->new(file => 'foobar.mp3', mime_type => 'audio/mpeg');
print "foobar.mp3 is ", $foo->duration, " seconds long.\n";

DESCRIPTION

What is a driver?

As this module uses the term, a driver supplys low-level functionality to a class, but does so by being a subclass instead of a parent class. When the main class's constructor is called, you get a more specific driver object back instead. Exactly what type of object you get back is up to the main class, and the object is expected to follow the main class's documented API. Common perl examples of classes that follow this sort of pattern are DBI's DBD classes (which handle interacting with different SQL backends), and libwww-perl's protocol classes (which handle talking to different types of servers: http, ftp, gopher, etc.)

The problem with driver class design

The common problem with driver class design is subclassing. There are two types of object you may want to extend: The base API class, or specific drivers.

Other object-oriented languages doublethink their way around the problem by making use of the decorator design pattern to accomplish this goal.

Decorators can get extremely cumbersome because they involve wrapping objects. Perl's AUTOLOAD method helps with this, but it's still a pain to end up with an object wrapped in an object that's wrapped in an object that's wraped in a...

Another problem with this approach is that there is no seperation between the low-level driver behaviour and the abstractions that stitch this low-level behaviour together. The solution here is to have an API class, and have low-level driver classes be subclasses of that. Then you could apply polymorphism or factory models to make sure your object gets the correct low-level methods.

The problems listed above are easy to solve on one level of class hierarchy. But what happens when you want to extend a base API? Or maybe a driver-specific API?

What is Class::Driver?

Class::Driver is a base class for modules that want to provide a unified interface to related operations that depend on a specific low-level driver. In Class::Driver, the design model of a class with drivers is:

There are base classes and driver classes. Driver classes inherit from base classes. When a constructor is called on a base class, it will attempt to find a more specific driver class to instantiate instead.

Class::Driver manages this, as well as making it easier to create subclasses of both the driver classes and their base classes.

Base classes can have subclasses that are not actually drivers. When these subclasses are constructed, we still want the functionality that is provided by the parent's driver classes.

I call this the "driver design pattern". It could be considered a sort of two-dimensional inheritance.

Several modules (such as DBI) cook this sort of functionality on their own. Class::Driver attempts to provide a unified interface for this functionality that makes issues such as subclassing less problematic. In the process, it might be creating more problems, but time will tell.

MECHANICS

Class::Driver does not provide you with a constructor. How you initialize your object is up to you. Instead it provides you with a class factory; you call driver_load on the class that you wish to use, specifying the driver you want. driver_load then creates a class hierarchy for the specific driver you want (or uses one if it's been created already).

This hierarchy will always be a subclass of both the driver-specific modules, and the "base" class(es) those drivers use, regardless of where specific drivers exist in your inheritance tree. Consider the following example (arrows represent @ISA relationships):

MyDriver
  ^
MySubDriver      <-      MySubDriver::foo
  ^                      MySubDriver::bar
  ^
MySub2Driver
  ^
MySub3Driver     <-      MySub3Driver::foo
  ^
MySub4Driver

If you call MySub4Driver-driver_load> asking for a 'foo' driver, stub classes will be generated to fill in the correct inheritance tree, and you'll get a "MySub4Driver::foo::_base" object back that inherits correctly:

MyDriver
  ^
MySubDriver      <-      MySubDriver::foo        <-      MySubDriver::foo::_hierarchy
  ^                      MySubDriver::bar                  ^
  ^                                                        ^
MySub2Driver                                             MySub2Driver::foo::_hierarchy
  ^                                                        ^
MySub3Driver     <-      MySub3Driver::foo               MySub3Driver::foo::_hierarchy
  ^                                                        ^
MySub4Driver                                             MySub4Driver::foo::_hierarchy

The _hierarchy packages also have a copy of any methods from related packages. So while MySub3Driver::foo::_hierarchy doesn't inherit from MySub3Driver::foo in the strictest sense, it does get a copy of any methods provided by that package (or it's base package, MySub3Driver). These methods are copied with Class::Clone so that the correct inheritance tree is followed when they call SUPER::. (A second type of package, "_base", is also generated so that SUPER:: relationships between a driver and the core API are maintained).

Any driver packages that don't exist (in this case, MySub2Driver::foo, and MySubDriver::foo, and MyDriver::foo) will be automatically generated (as stub classes that just inherit from their parent class) along the way. You can specify whether a driver needs to exist (or if it's okay to get all stub classes back), as well as what classes absolutely *must* have an immediate driver available; see "driver_required" and "driver_required_here" below.

The end result should be that you can create base classes, drivers, and sub-classes and sub-drivers and everything should inherit properly.

HOW TO USE THIS MODULE

The basest class of your driverish module should inherit from Class::Driver.
Specify how and when a specific driver is required.

This is done by adding two class methods to your module. Each method should return true if you want your module to require it's condition, false if not. "driver_load" will die if you require a condition that has not been met. You should supply both in your basest package, and subclasses can provide them as well:

driver_required

At least one real driver class or sub-class is required; it isn't satisfactory for *everything* to be a stub.

Example where you'd return true: a module allows you to export to a variety of file formats, each of which uses it's own driver (eg: png, xml, or csv). Subclasses (say, a module that can compose music out of your data) can specify their own drivers (such as mp3).

Example where you'd return false: a database-driven module whose base class provides ANSI SQL queries, and driver classes provide some alternatives to deal with small differences on some database engines.

driver_required_here

This class (and sub-classes that don't override driver_required_here) must have a driver class for the driver in question. It's not good enough that a sub (or super) class provides this driver.

Example where you'd return true: a video-driver abstraction module (maybe it picks between XWindows and M$Windows implementations). Subclasses can extend functionality, but low-level drivers should be driver classes of this package.

You almost always want to set this false on any sub-classes that set it to true. Otherwise it's value will be inherited, and your sub-class will also have to provide each and every driver for itself (which kind of defeats the purpose of Class::Driver to begin with!)

Example where you'd return false on the basest class: the database-driven module described in driver_required would also want to set driver_required_here to false, since that class would be providing sensible default behaviour for all drivers.

By default, driver_required_here and driver_required return false, meaning no specific drivers have to exist at all.

Write a driver_new constructor on your basest class

The driver_new constructor should return a blessed reference to the class it is called from. If you wanted a hash reference created out of the arguments passed in, you'd do something like:

sub driver_new {
  my($class, %args) = @_;
  my $self = bless \%args, $class;
  return $self;
}

You can also provide this method on base classes, which should usually return something like:

$class->SUPER::new(...)

instead.

Write your constructor on your basest class

Your constructor's job is to accept arguments, decide what driver we are going to use, and call driver_load, passing in the name of the driver we are using, and any other arguments your driver_new constructor is expecting. For example, if your constructor was called new and you wanted to detect the driver based on the extension of a filename passed in:

sub new {
    my($class, %args) = @_;
    if($args{file} && $args{file} =~ m{^.+\.(.+?)$}) {
        return $class->driver_load($1, %args);
    } else {
        die "Couldn't figure out what driver to use";
    }
}

(You usually want to sanitize your input a bit better than that, but you get the picture.)

The driver's name as passed into driver_load should be a valid fragment of a class name (eg; foo, foo::bar and foo_bar are okay, foo-bar is not.) This string will be used to find your driver-specific packages when searching the namespace of your package and parent packages.

When you call driver_load, it will attempt to find the appropriate driver, obeying the rules you laid down in driver_required and driver_required_here, creating any missing stub drivers and class hierarchies as it goes.

driver_load will then call your driver_new constructor on the resulting class, passing in any extra arguments it received.

Write a driver class (or classes)

A driver class is inside your package's namespace. Eg; a driver for "png" files in "MyDataExporter" would be found in the package "MyDataExporter::png". It should inherit from your package ("MyDataExporter::png" @ISA "MyDataExporter"). It must also supply one method that indicates that it is a driver, called "driver". This method should return the driver's name. Eg;

sub driver { "png"; }

METHODS

The following public methods are available for use in Class::Driver:

driver()

Returns a driver name if this is a driver class, or false if this is a generic class. You should override this in driver classes; see driver in "HOW TO USE THIS MODULE" for details.

driver_load($driver, @args)

Attempt to load the driver $driver, generating any class hierarchies that are required. If successful, call it's driver_new method, passing in @args. If unsuccessful, and a driver is required, die. If a driver is not required, just instantiate the base class with driver_new. See "HOW TO USE THIS MODULE" for more information on driver_load and driver_new.

driver_derived()

Always returns true. If a package can invoke the driver_derived method, Class::Driver assumes this is a driver class it's dealing with. Only driver_derived classes have their methods copied; other classes are imported instead. This ensures that only methods that are expecting their notion of SUPER:: to be rewritten actually have that happen.

driver_hierarchy()

Returns true if this class is a generated class hierarchy, false otherwise.

driver_base()

Returns true if this class is a generated base class, false otherwise.

driver_stub()

Returns true if this is a stub driver class created by driver_create_stub, false otherwise.

driver_required()

See driver_required in "HOW TO USE THIS MODULE" for details.

driver_required_here()

See driver_required_here in "HOW TO USE THIS MODULE" for details.

driver_has_driver($driver)

If we have a specific driver class for $driver, return true. Otherwise, return the result of driver_has_superdriver.

driver_has_superdriver($driver)

Returns true if any of our parent classes has a specific driver for $driver, false otherwise. This method will die if it encounters a class that doesn't have a driver that requires it there.

driver_package_name($driver)

Return the package name for a driver class for $driver in the current class. By default, this just appends "::$driver" to the current class's name. If $driver contains data that is not suitable for a class name, driver_package_name dies.

You may want to override this method if you keep your driver classes elsewhere, eg;

# drivers are in Package::Driver::whatever
sub driver_package_name {
  my($class, $driver) = @_;
  return $class->driver_sane_package(join('::', $class, 'Driver', $driver));
}

See also driver_sane_package below.

driver_sane_package($package)

Validate $package as a valid perl package name. If it contains funny characters or results in a syntax error when used as a package name, die with a fatal error. Otherwise, return true.

SEE ALSO

Class::Mutator, Class::Role, Class::Clone

AUTHOR

Tyler "Crackerjack" MacDonald <japh@crackerjack.net>

LICENSE

Copyright 2006 Tyler MacDonald.

This is free software; you may redistribute it under the same terms as perl itself.