NAME
Class::Driver - Generate driver ("composite") class heiarchies 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 stich 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 heiarchy. But what happens when you want to extend to 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. It instead 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 heiarchy for the specific driver you want (or uses one if it's been created already).
This heiarchy 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
^
MySubSubDriver
^
MySubSubSubDriver <- MySubSubSubDriver::foo
^
MySubSubSubSubDriver
If you call MySubSubSubSubDriver-
driver_load> asking for a 'foo' driver, stub classes will be generated to fill in the correct inheritance tree, and you'll get a "MySubSubSubSubDriver::foo::_base" object back that inherits correctly:
MyDriver
^
MySubDriver <- MySubDriver::foo <- MySubDriver::foo::_heiarchy
^ MySubDriver::bar ^
^ ^
MySubSubDriver MySubSubDriver::foo::_heiarchy
^ ^
MySubSubSubDriver <- MySubSubSubDriver::foo MySubSubSubDriver::foo::_heiarchy
^ ^
MySubSubSubSubDriver MySubSubSubSubDriver::foo::_heiarchy
The _heiarchy packages also have a copy of any methods from related packages. So while MySubSubSubDriver::foo::_heiarchy doesn't inherit from MySubSubSubDriver::foo in the strictest sense, it does get a copy of any methods provided by that package (or it's base package, MySubSubSubDriver). 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, MySubSubDriver::foo, and MySubDriver::foo, and MyDriver::foo) will be automatically genereated (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 putting two classes in 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 in herited, and your sub-class will also have to provide each any 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 are 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 yourdriver_new
constructor is expecting. For example, if your constructor was callednew
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
andfoo_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 heiarchies as it goes.
driver_load
will then call yourdriver_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 pacakge ("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 heiarchies that are required. If successful, call it'sdriver_new
method, passing in@args
. If unsuccessful, and a driver is required, die. If a driver is not required, just instantiate the base class withdriver_new
. See "HOW TO USE THIS MODULE" for more information ondriver_load
anddriver_new
. - driver_derived()
-
Always returns true. If a package
can
invoke thedriver_derived
method,Class::Driver
assumes this is a driver class we're dealing with. Onlydriver_derived
classes have their methods copied; other classes are imported instead. This ensures that only methods that are expecting their notion ofSUPER::
to be rewritten actually have that happen. - driver_heiarchy()
-
Returns true if this class is a generated class heiarchy, 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 ofdriver_has_superdriver
. - driver_has_superdriver($driver)
-
If any of our parent classes, has a specific driver for
$driver
, return true, otherwise return false. 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 2005 Tyler MacDonald.
This is free software; you may redistribute it under the same terms as perl itself.