Perl Best Practices
16.11. Cumulative Methods
Use :CUMULATIVE methods instead of SUPER:: calls. One of the most important advantages of using the BUILD( ) and DEMOLISH( ) mechanisms supplied by Class::Std is that those methods don't require nested calls to their ancestral methods via the SUPER pseudoclass. The constructor and destructor provided by Class::Std take care of the necessary redispatching automatically. Each BUILD( ) method can focus solely on its own responsibilities; it doesn't have to also help orchestrate the cumulative constructor effects across the class hierarchy by remembering to call $self->SUPER::BUILD( ). This approach produces far more reliable class implementations, because forgetting to include the SUPER call in a "chained" constructor or destructor will immediately terminate the chain of calls, disenfranchising all the remaining construction/destruction methods higher up in the class's hierarchy. Moreover, calls via SUPER can only ever call the method of exactly one ancestral class, which is not sufficient under multiple inheritance. This second problem can be solved in various ways (for example, by using the standard NEXT module), but all those solutions still rely on developers remembering to add the necessary code to every method in every class in order to continue the chain of calls. So all those solutions are inherently fragile. Class::Std provides a different way of creating methods whose effects accumulate through a class hierarchy, in the same way as those of BUILD( ) and DEMOLISH( ) do. Specifically, the module allows you to define your own cumulative methods. An ordinary non-cumulative method hides any method of the same name inherited from any base class, so when a non-cumulative method is called, only the most-derived version of it is ever invoked. In contrast, a cumulative method doesn't hide ancestral methods of the same name; it assimilates them. When a cumulative method is called, the most-derived version of it is invoked, then any parental versions, then any grandparental versions, and so on, until every cumulative method of the same name throughout the entire hierarchy has been called. For example, you could add a cumulative describe( ) method to the various wax and topping classes from Example 16-10 as follows: package Wax::Floor; use Class::Std; { my %name_of :ATTR( init_arg => 'name' ); my %patent_of :ATTR( init_arg => 'patent' ); sub describe :CUMULATIVE { my ($self) = @_; print "The floor wax $name_of{ident $self} ", "(patent: $patent_of{ident $self})\n"; return; } } package Topping::Dessert; use Class::Std; { my %name_of :ATTR( init_arg => 'name' ); my %flavour_of :ATTR( init_arg => 'flavour' ); sub describe :CUMULATIVE { my ($self) = @_; print "The dessert topping $name_of{ident $self} ", "with that great $flavour_of{ident $self} taste!\n"; return; } } package Shimmer; use base qw( Wax::Floor Topping::Dessert ); use Class::Std; { my %name_of :ATTR( init_arg => 'name' ); my %patent_of :ATTR( init_arg => 'patent' ); sub describe :CUMULATIVE { my ($self) = @_; print "New $name_of{ident $self} (patent: $patent_of{ident $self})\n", "Combining...\n"; return; } }
Because the various describe( ) methods are marked as being cumulative, a subsequent call to: my $product = Shimmer->new({ name=>'Shimmer', patent=>1562516251, flavour=>'Vanilla' }); $product->describe( );
will work its way up through the classes of Shimmer's inheritance tree (in the same order as a destructor call would), calling each describe( ) method it finds along the way. So the single call to describe( ) would invoke the corresponding method in each class, producing: New Shimmer (patent: 1562516251) Combining... The floor wax Shimmer (patent: 1562516251) The dessert topping Shimmer with that great Vanilla taste!
Note that the accumulation of describe( ) methods is hierarchical, and dynamic in nature. That is, each class only sees those cumulative methods that are defined in its own package or in one of its ancestors. So calling the same describe( ) on a base class object: my $wax = Wax::Floor->new({ name=>'Shimmer ', patent=>1562516251 }); $wax->describe( ); invokes only the corresponding cumulative methods from that point on up the hierarchy, and hence prints only: The floor wax Shimmer (patent: 1562516251)
Cumulative methods also accumulate their return values. In a list context, they return a (flattened) list that accumulates the lists returned by each individual method invoked. In a scalar context, a set of cumulative methods returns an object that, in a string context, concatenates individual scalar returns to produce a single string. For example, if the classes each have a cumulative method that returns their list of sales features: package Wax::Floor; use Class::Std; { sub feature_list :CUMULATIVE { return ('Long-lasting', 'Non-toxic', 'Polymer-based'); } } package Topping::Dessert; use Class::Std; { sub feature_list :CUMULATIVE { return ('Low-carb', 'Non-dairy', 'Sugar-free'); } } package Shimmer; use Class::Std; use base qw( Wax::Floor Topping::Dessert ); { sub feature_list :CUMULATIVE { return ('Multi-purpose', 'Time-saving', 'Easy-to-use'); } } then calling feature_list( ) in a list context: my @features = Shimmer->feature_list( ); print "Shimmer is the @features alternative!\n";
would produce a concatenated list of features, which could then be interpolated into a suitable sales pitch: Shimmer is the Multi-purpose Time-saving Easy-to-use Long-lasting Non-toxic Polymer-based Low-carb Non-dairy Sugar-free alternative! Finally, it's also possible to specify a set of cumulative methods that start at the base class(es) of the hierarchy and work downwards, the way BUILD( ) does. To get that effect, mark each method with :CUMULATIVE(BASE FIRST), instead of just :CUMULATIVE. For example: package Wax::Floor; use Class::Std; { sub active_ingredients :CUMULATIVE(BASE FIRST) { return "\tparadichlorobenzene, cyanoacrylate, peanuts (in wax)\n"; } } package Topping::Dessert; use Class::Std; { sub active_ingredients :CUMULATIVE(BASE FIRST) { return "\tsodium hypochlorite, isobutyl ketone, ethylene glycol " . "(in topping)\n"; } } package Shimmer; use Class::Std; use base qw( Wax::Floor Topping::Dessert ); { sub active_ingredients :CUMULATIVE(BASE FIRST) { return "\taromatic hydrocarbons, xylene, methyl mercaptan (in binder)\n"; } } So a scalar-context call to active_ingredients( ): my $ingredients = Shimmer->active_ingredients( ); print "May contain trace amounts of:\n$ingredients";
would start in the base classes and work downwards, concatenating base-class ingredients before those of the derived class, to produce: May contain trace amounts of: paradichlorobenzene, cyanoacrylate, peanuts (in wax) sodium hypochlorite, isobutyl ketone, ethylene glycol (in topping) aromatic hydrocarbons, xylene, methyl mercaptan (in binder)
Note that you can't specify both :CUMULATIVE and :CUMULATIVE(BASE FIRST) on methods of the same name in the same hierarchy. The resulting set of methods would have no well-defined invocation order, so Class::Std throws a compile-time exception instead. |