|
NAMEClass::Delegation - Object-oriented delegationVERSIONThis document describes version 1.9.0 of Class::Delegation released April 23, 2002.SYNOPSISpackage 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', ; BACKGROUND[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 qw( Wheel ); package Left_Rear_Wheel; use base qw( Wheel ); package Right_Front_Wheel; use base qw( Wheel ); package Right_Rear_Wheel; use base qw( 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. The problem(s) with attribute-based hierarchiesUsing 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. DESCRIPTIONThe 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:
The syntax and semantics of delegationTo 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', ; Specifying methods to be delegatedThe 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:
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. Specifying attributes to be delegated toThe 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, Specifying the name of a delegated methodSometimes 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 } Delegation to selfClass::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'], Handling failure to delegateIf 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.EXAMPLESDelegation is a useful replacement for inheritance in a number of contexts. This section outlines five of the most common uses.Simulating single inheritanceUnlike 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. Replacing method dispatch semanticsAnother 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. Simulating multiple inheritance of pseudohashsAnother 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". Adapting legacy codeBecause 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. Multiplexing a facadeThe 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 Laevorotatory 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); AUTHORDamian Conway (damian@conway.org)BUGSThere are undoubtedly serious bugs lurking somewhere in this code. Bug reports and other feedback are most welcome.COPYRIGHTCopyright (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.
Visit the GSP FreeBSD Man Page Interface. |