| Class-Delegation documentation | view source | Contained in the Class-Delegation distribution. |
Class::Delegation - Object-oriented delegation
This document describes version 1.06 of Class::Delegation, released April 23, 2002.
package Car;
use Class::Delegation
send => 'steer',
to => ["left_front_wheel", "right_front_wheel"],
send => 'drive',
to => ["right_rear_wheel", "left_rear_wheel"],
as => ["rotate_clockwise", "rotate_anticlockwise"]
send => 'power',
to => 'flywheel',
as => 'brake',
send => 'brake',
to => qr/.*_wheel$/,
send => 'halt'
to => -SELF,
as => 'brake',
send => qr/^MP_(.+)/,
to => 'mp3',
as => sub { $1 },
send => -OTHER,
to => 'mp3',
send => 'debug',
to => -ALL,
as => 'dump',
send => -ALL,
to => 'logger',
;
[Skip to "DESCRIPTION" if you don't care why this module exists]
Inheritance is one of the foundations of object-oriented programming. But inheritance has a fundamental limitation: a class can only directly inherit once from a given parent class. This limitation occasionally leads to awkward work-arounds like this:
package Left_Front_Wheel; use base wq( Wheel ); package Left_Rear_Wheel; use base wq( Wheel ); package Right_Front_Wheel; use base wq( Wheel ); package Right_Rear_Wheel; use base wq( Wheel ); package Car; use base qw(Left_Front_Wheel Left_Rear_Wheel Right_Front_Wheel Right_Rear_Wheel);
Worse still, the method dispatch semantics of most languages (including Perl) require that only a single inherited method (in Perl, the one that is left-most-depth-first in the inheritance tree) can handle a particular method invocation. So if the Wheel class provides methods to steer a wheel, drive a wheel, or stop a wheel, then calls such as:
$car->steer('left');
$car->drive(+55);
$car->brake('hard');
will only be processed by the left front wheel. This will probably not produce desirable road behaviour.
It is often argued that it is simply a synecdochic mistake to treat a car as a specialized form of four wheels, but this argument is far from conclusive. And, regardless of its philosophical merits, programmers often do conceptualize composite systems in exactly this way.
The alternative is, of course, to make the four wheels attributes of the class, rather than ancestors:
package Car;
sub new {
bless { left_front_wheel => Wheel->new('steer', 'brake'),
left_rear_wheel => Wheel->new('drive', 'brake'),
right_front_wheel => Wheel->new('steer', 'brake'),
right_rear_wheel => Wheel->new('drive', 'brake'),
}, $_[0];
}
Indeed some object-oriented languages (e.g. Self) do away with inheritance entirely and rely exclusively on the use of attributes to implement class hierarchies.
Using attributes instead of inheritance does solve the problem: it allows a Car to directly have four wheels. However, this solution creates a new problem: it requires that the class manually redispatch (or delegate) every method call:
sub steer {
my $self = shift;
return ( $self->{left_front_wheel}->steer(@_),
$self->{right_front_wheel}->steer(@_), );
}
sub drive {
my $self = shift;
return ( $self->{left_rear_wheel}->drive(@_),
$self->{right_rear_wheel}->drive(@_), );
}
sub brake {
my $self = shift;
return ( $self->{left_front_wheel}->brake(@_),
$self->{left_rear_wheel}->brake(@_),
$self->{right_front_wheel}->brake(@_),
$self->{right_rear_wheel}->brake(@_), );
}
AUTOLOAD methods can help in this regard, but usually at the cost of
readability and maintainability:
sub AUTOLOAD {
my $self = shift;
$AUTOLOAD =~ s/.*:://;
my @results;
return map { $self->{$_}->$AUTOLOAD(@_) },
grep { $self->{$_}->can($AUTOLOAD) },
keys %$self;
}
Often, the simple auto-delegation mechanism shown above cannot
be used at all, and the various cases must be hand-coded into the AUTOLOAD
or into separate named methods (as shown earlier).
For example, an electric car might also have a flywheel and an MP3 player:
sub new {
bless { left_front_wheel => Wheel->new('steer', 'brake'),
left_rear_wheel => Wheel->new('drive', 'brake'),
right_front_wheel => Wheel->new('steer', 'brake'),
right_rear_wheel => Wheel->new('drive', 'brake'),
flywheel => Flywheel->new(),
mp3 => MP3::Player->new(),
}, $_[0];
}
The Flywheel class would probably have its own brake method (to
harvest motive energy from the flywheel) and MP3::Player might have its
own drive method (to switch between storage devices).
An AUTOLOAD redispatch such as that shown above would then fail very
badly. Whilst it would prove merely annoying to have one's music skip
tracks ($self->{mp3}->drive(+10)) every time one accelerated
($self->{right_rear_wheel}->drive(+10)), it might be disastrous to
attempt to suck energy out of the flywheel
($self->{flywheel}->brake()) whilst the brakes are trying to feed it
back in ($self->{right_rear_wheel}->brake()).
Class-action lawyers love this kind of programming.
The Class::Delegation module simplifies the creation of delegation-based class hierarchies, allowing a method to be redispatched:
These three delegation mechanisms can be specified for:
To cause a hash-based class to delegate method invocations to its attributes, the Class::Delegation module is imported into the class, and passed a list of method/handler mappings that specify the delegation required. Each mapping consists of between one and three key/value pairs. For example:
package Car;
use Class::Delegation
send => 'steer',
to => ["left_front_wheel", "right_front_wheel"],
send => 'drive',
to => ["right_rear_wheel", "left_rear_wheel"],
as => ["rotate_clockwise", "rotate_anticlockwise"]
send => 'power',
to => 'flywheel',
as => 'brake',
send => 'brake',
to => qr/.*_wheel$/,
send => qr/^MP_(.+)/,
to => 'mp3',
as => sub { $1 },
send => -OTHER,
to => 'mp3',
send => 'debug',
to => -ALL,
as => 'dump',
send => -ALL,
to => 'logger',
;
The names of methods to be redispatched can be
specified using the 'send' key. They may be specified as single strings, arrays of strings, regular
expressions, subroutines, or as one of the two special names: -ALL and -OTHER.
A single string specifies a single method to be delegated in some way.
The other alternatives specify sets of methods
that are to share the associated delegation semantics. That set
of methods may be specified:
explicitly, by an array (the set consists of those method calls whose names appear in the array),
implicitly, by a regex (the set consists of those method calls whose names match the pattern),
procedurally, by a subroutine (the set consists of any method calls for which the subroutine returns a true value, when passed the method invocant, the method name, and the arguments with which the method was invoked),
generically, by -ALL (the set consists of every method call -- excluding calls
to DESTROY -- that is not handled by an
explicit method of the class),
exclusively, by -OTHER (the set consists of every method call -- excluding calls
to DESTROY -- that is not successfully
delegated by any earlier mapping in the use Class::Delegation list).
The exclusion of calls to DESTROY in the last two cases ensures that automatically
invoked destructor calls are not erroneously delegated. DESTROY calls can be
delegated through any of the other specification mechanisms.
The actual delegation behaviour is determined by the attributes to which these
methods are to be delegated. This information can be specified via the 'to'
key, using a string, an array, a regex, a subroutine, or the special flag
-ALL. Normally the delegated method that is invoked on the specified attribute (or attributes)
has the same name as the original call, and is invoked in the same calling
context (void, scalar, or list).
If the attribute is specified via a single string, that string is taken
as the name of the attribute to which the associated method (or methods)
should be delegated. For
example, to delegate invocations of $self->power(...) to
$self->{flywheel}->power(...):
use Class::Delegation
send => 'power',
to => 'flywheel';
If the attribute is specified via a single string that starts with "-...">
then that string is taken as specifying the name of a method of the
current object. That method is called and is expected to return an
object. The original method that was being delegated is then delegated to that
object. For example, to delegate invocations of $self->power(...) to
$self->flywheel()->power(...):
use Class::Delegation
send => 'power',
to => '->flywheel';
Since this syntax is a little obscure (and not a little ugly), the same effect can also be obtained like so:
use Class::Delegation
send => 'power',
to => -SELF->flywheel;
An array reference can be used in the attribute position to specify the a list of attributes, all of which are delegated to -- in sequence they appear in the list. Note that each element of the array is processed recursively, so it may contain any of the other attribute specifiers described in this section (or, indeed, a nested array of attribute specifiers)
For example, to distribute invocations of $self->drive(...) to both
$self->{left_rear_wheel}->drive(...) and$self->{right_rear_wheel}->drive(...):
use Class::Delegation
send => 'drive',
to => ["left_rear_wheel", "right_rear_wheel"];
Note that using an array to specify parallel delegation has an effect on the return
value of the original method. In a scalar context, the original call returns a reference to
an array containing the (scalar context) return values of each of the calls. In
a list context, the original call returns a list of array references
containing references to the individual (list context) return lists of the calls. So, for example, if a
class's cost method were delegated like so:
use Class::Delegation
send => 'cost',
to => ['supplier', 'manufacturer', 'distributor'];
then the total cost could be calculated like this:
use List::Util 'sum';
$total = sum @{$obj->cost()};
Specifying the attribute as a regular expression causes the associated
method to be delegated to any attribute whose name matches the pattern.
Attributes are tested for such a match -- and delegated to -- in the
internal order of their hash (i.e. in the sequence returned by keys). For
example, to redispatch brake calls to every attribute whose name ends in "_wheel":
send => 'brake',
to => qr/.*_wheel$/,
If a subroutine reference is used as the 'to' attribute specifier, it is passed the
invocant, the name of the method, and the argument list. It is expected to
return either a value specifying the correct attribute name (or names). As with an
array, the value returned may be any valid attribute specifier (including
another subroutine reference) and is iteratively processed to determine the
correct target(s) for delegation.
A subroutine may also return a reference to an object, in which case the subroutine is delegated to that object (rather than to an attribute of the current object). This can be useful when the actual delegation target is more complex than just a direct attribute. For example:
send => 'start',
to => sub { $_[0]{ignition}{security}[$_[0]->next_key] },
If the -ALL flag is used as the name of the attribute, the method
is delegated to all attributes of the object (in their keys order). For
example, to forward debugging requests to every attribute in turn:
send => 'debug',
to => -ALL,
Sometimes it is necessary to invoke an attribute's method through a
different name than that of the original delegated method. The 'as'
key facilitates this type of method name translation in any delegation.
The value associated with an 'as' key specifies the name of the
method to be invoked, and may be a string, an array, or a subroutine.
If a string is provided, it is used as the new name of the delegated method.
For example, to cause calls to $self->power(...)
to be delegated to $self->{flywheel}->brake(...):
send => 'power',
to => 'flywheel',
as => 'brake',
If an array is given, it specifies a list of delegated method names.
If the 'to' key specifies a single attribute, each method in the list is
invoked on that one attribute. For example:
send => 'boost',
to => 'flywheel',
as => ['override', 'engage', 'discharge'],
would sequentially call:
$self->{flywheel}->override(...);
$self->{flywheel}->engage(...);
$self->{flywheel}->discharge(...);
If both the 'to' key and the 'as' key specify multiple values, then
each attribute and method name form a pair, which is invoked. For example:
send => 'escape',
to => ['flywheel', 'smokescreen'],
as => ['engage', 'release'],
would sequentially call:
$self->{flywheel}->engage(...);
$self->{smokescreen}->release(...);
If a subroutine reference is used as the 'as' specifier, it is passed the
invocant, the name of the method, and the argument list, and is expected to
return a string that will be used as the method name. For example, to
strip method calls of a "driver_..." prefix and delegate them to the
'driver' attribute:
send => sub { substr($_[1],0,7) eq "driver_" },
to => 'driver',
as => sub { substr($_[1],7) }
or:
send => qr/driver_(.*)/,
to => 'driver',
as => sub { $1 }
Class::Delegation can also be used to delegate methods back to the original
object, using the -SELF option with the 'to' key. For example, to
redirect any call to overdrive so to invoke the boost method instead:
send => 'overdrive',
to => -SELF,
as => 'boost',
Note that this only works if the object does not already have an
overdrive method.
As with other delegations, a single call can be redelegated-to-self as multiple calls. For example:
send => 'emergency',
to => -SELF,
as => ['overdrive', 'launch_rockets'],
If a method cannot be successfully delegated through any of its mappings,
Class::Delegation will ignore the call and the built-in
AUTOLOAD mechanism will attempt to handle it instead.
Delegation is a useful replacement for inheritance in a number of contexts. This section outlines five of the most common uses.
Unlike most other OO languages, inheritance in Perl only works well when
the base class has been designed to be inherited from. If the attributes
of a prospective base class are inaccessible, or the implementation is
not extensible (e.g. a blessed scalar or regular expression), or the
base class's constructor does not use the two-argument form bless, it
will probably be impractical to inherit from the class.
Moreover, in many cases, it is not possible to tell -- without a detailed inspection of a base class's implementation -- whether such a class can easily be inherited. This inability to reliably treat classes as encapsulated and implementation-independent components seriously undermines the usability of object-oriented Perl.
But since inheritance in Perl merely specifies where a class is to look next if a suitable method is not found in its own package [3], it is often possible to replace derivation with aggregation and use a delegated attribute instead.
For example, it is possible to simulate the inheritance of the class Base via a delegated attribute:
package Derived;
use Class::Delegation send => -ALL, to => 'base';
sub new {
my ($class, $new_attr1, $new_attr2, @base_args) = @_;
bless { attr1 => $new_attr1,
attr2 => $new_attr2,
base => Base->new(@base_args),
}, $class;
}
Now any method that is not present in Derived is delegated to the Base object
referred to by the base attribute, just as it would have been if
Derived actually inherited from Base.
This technique works in situations where the functionality of the Base methods is non-polymorphic with respect to their invocant. That is, if an inherited method in class Base were to interrogate the class of the object on which it was called, it would find a Derived object. But a delegated method in class Base will find a Base object. This is not the usual behaviour in OO Perl, but is correct and appropriate under the earlier assumption that Base has not been designed to be inherited from -- and must therefore always expect a Base class object as its invocant.
Another situation in which delegation is preferable to inheritance is where inheritance is feasible, but Perl's standard dispatch semantics -- left-most, depth-first priority of method dispatch -- are inappropriate.
For example, if various base classes in a class hierarchy provide a dump_info method
for debugging purposes, then a derived class than multiply inherits from two or more
of those classes will only dispatch calls to dump_info to the left-most ancestor's
method. This is unlikely to be the desired behaviour.
Using delegation it is possible to cause calls to dump_info to invoke the corresponding
methods of all the base classes, whilst all other method calls are dispatched left-most and
depth-first, as normal:
package Derived;
use Class::Delegation
send => 'dump_info',
to => -ALL,
send => -OTHER,
to => 'base1',
send => -OTHER,
to => 'base2',
;
sub new {
my ($class, %named_args) = @_;
bless { base1 => Base1->new(%named_args),
base2 => Base2->new(%named_args),
}, $class;
}
Note that the semantics of send => -OTHER ensure that only one of the
two base classes is delegated a method. If base1 is able to handle
a particular method delegation, then it will have been dispatched when
the -OTHER governing base2 is reached, so the second -OTHER will
ignore it.
Another situation in which multiple inheritance can cause trouble is
where a class needs to inherit from two base classes that are both
implemented via pseudohashes. Because each pseudohash base class will
assume that its attributes start from index 1 of the pseudohash
array, the methods of the two classes would contend for the same
attribute slots in the derived class. Hence the use base pragma
detects cases where two ancestral classes are pseudohash-based and
rejects them (terminally).
Delegation provides a convenient way to provide the effects of pseudohash multiple inheritance, without the attendant problems. For example:
package Derived;
use Class::Delegation
send => -ALL,
to => 'pseudobase1',
send => -OTHER,
to => 'pseudobase2',
;
sub new {
my ($class, %named_args) = @_;
bless { pseudobase1 => Pseudo::Base1->new(%named_args),
pseudobase2 => Pseudo::Base2->new(%named_args),
}, $class;
}
As in the previous example, only one of the two base classes
is delegated a method. The -ALL associated with pseudobase1
attempts to delegate every method to that attribute, then the -OTHER
associated with pseudobase2 catches any methods that cannot be
handled by pseudobase1.
Because the 'as' key can take a subroutine, it is also possible to
use a delegating class to adapt the interface of an existing class. For example,
a class with separate "get" and "set" accessors:
class DogTag;
sub get_name { return $_[0]->{name} }
sub set_name { $_[0]->{name} = $_[1] }
sub get_rank { return $_[0]->{rank} }
sub set_rank { $_[0]->{rank} = $_[1] }
sub get_serial { return $_[0]->{serial} }
sub set_serial { $_[0]->{serial} = $_[1] }
# etc.
could be trivially adapted to provide combined get/set accessors like so:
class DogTag::SingleAccess;
use Class::Delegation
send => -ALL
to => 'dogtag',
as => sub {
my ($invocant, $method, @args) = @_;
return @args ? "set_$method" : "get_$method"
},
;
sub new { bless { dogtag => DogTag->new(@_[1..$#_) }, $_[0] }
Here, the 'as' subroutine determines whether an "new value" argument
was passed to the original method, delegating to the set_... method if so,
and to the get_... method otherwise.
The ability to use regular expressions to specify method names, and subroutines to indicate the attributes and attribute methods to which they are delegated, opens the possibility of creating a class that acts as a collective front-end for several others. For example:
package Bilateral;
%Bilateral = ( left => 'Levorotatory',
right => 'Dextrorotatory',
);
use Class::Delegation
send => qr/(left|right)_(.*)/,
to => sub { $1 },
as => sub { $2 },
;
sub AUTOLOAD {
carp "$AUTOLOAD does not begin with 'left_...' or 'right_...'"
},
The Bilateral class now forwards all class method calls that are prefixed
with "left_..." to the Lævorotatory class, and all those prefixed with
"right_..." to the Dextrorotatory class. Any calls that cannot be dispatched
are caught and ignored (with a warning) by the AUTOLOAD.
The mechanism by which the class method dispatch is achieved is perhaps a little obscure. Consider the invocation of a class method:
Bilateral->left_rotate(45);
Here, the invocant is the string "Bilateral", rather than a blessed object. Thus,
when Class::Delegation forwards the call to:
$self->{$1}->$2(45);
the effect is the same as calling:
"Bilateral"->{left}->rotate(45);
This invokes a little-known feature of the -> operator [4]. If a hash access is
performed on a string, that string is taken as a symbolic
reference to a package hash variable in the current package. Thus the above call is internally translated to:
${"Bilateral"}{left}->rotate(45);
which is equivalent to the class method call:
Levorotatory->rotate(45);
Damian Conway (damian@conway.org)
There are undoubtedly serious bugs lurking somewhere in this code. Bug reports and other feedback are most welcome.
Copyright (c) 2001, Damian Conway. All Rights Reserved.
This module is free software. It may be used, redistributed
and/or modified under the same terms as Perl itself.
| Class-Delegation documentation | view source | Contained in the Class-Delegation distribution. |