We don't bite newbies here... much | |
PerlMonks |
comment on |
( [id://3333]=superdoc: print w/replies, xml ) | Need Help?? |
I've recently stumbled upon a technique for creating mixin classes which I thought might be worthy of some discussion here. Motivation
As an example, I've recently been refactoring Text::MicroMason to allow dynamic subclassing. The Text::MicroMason::Base class provides methods to convert templates from HTML::Mason's syntax to runnable Perl code. Other classes extend that behavior in different ways, such as running code in a Safe compartment or adding support for the "|h" and "|u" filter flags. A naive implementation of this might use a subclass for each behaviour, and look like the following: +---------+ | Base | +---------+ | +------------------+------------------+ v v v +---------+ +---------+ +---------+ | Cache | | Filter | | Safe | @ISA=qw(Base) +---------+ +---------+ +---------+ The well-known problem with this implementation appears when you want to combine several features: +---------+ | Base | +---------+ | +------------------+------------------+ v v v +---------+ +---------+ +---------+ | Cache | | Filter | | Safe | @ISA=qw(Base) +---------+ +---------+ +---------+ | | +---------+--------+ v +-------------+ | Safe_Filter | @ISA=qw(Filter Safe); +-------------+ This is the dreaded "diamond inheritance" problem: if Base provides a compile() method, which Filter and Safe each override to perform additional actions around a call to SUPER::compile, how can we ensure they are all called in sequence? Diamond inheritance is avoided if we use mixin classes, which don't themselves inherit from the base class: +---------+ +---------+ +---------+ | Base | | Filter | | Safe | +---------+ +---------+ +---------+ | | | +------------------+------------------+ v +-------------+ | Safe_Filter | @ISA=qw(Filter Safe Base); +-------------+ However, in this condition our mixin classes can't call SUPER methods at all! Combining Mixins with Run-time Evals
In this implementation, the mixin classes can both define some methods normally, and also provide some method definitions in string form, as part of a @MIXIN array. When a request for a class with a particular combination of features is received, a series of new classes are created on the fly. # User calls Text::MicroMason->new( '-Filter', '-Safe' ) +---------+ +---------+ +---------+ | Base | | Filter | | Safe | +---------+ +---------+ +---------+ | | | +-----------+----------+ | v | +-------------+ eval join "\n", | | Filter_Base | '@ISA=qw(Filter Base);',| +-------------+ @Filter::MIXIN; | | | +-----------+---------------------+ v +----------------+ eval join "\n", |Safe_Filter_Base| '@ISA=qw(Safe Filter_Base);', +----------------+ @Safe::MIXIN; Although string evals are notoriously slow, we only need to do this once for each new combination of mixins, so the run-time impact is minimal. If the mixin classes define their compile() methods in @MIXIN, calling the compile() method on a Safe_Filter_Base object will now properly follow SUPER calls up the inheritance chain. And of course calls to the other, non-SUPER-calling methods will continue to follow the hierarchy and perform as expected. A typical definition for a method that would be defined in the @MIXIN array might look like the following example. The BEGIN line snags the file name and line so that error messages are reported in the right place, and captures the text of the following subroutine with a here-doc.
The order in which mixins are stacked is significant, so the caller does need to have some understanding of how their behaviors interact. For example, you'd typically want to ensure that the Benchmarking mixin was the first in the chain, so that it could time everything later in the sequence. Related Techniques
There are some good non-inheritance approaches to this class of problem, particularly the use of decorators and delegation; for example the Benchmarking object could hold a reference to the base object, and the base object could accept an optional reference to a separate Lexer object. While I'm a big fan of composition, I also like the way this mixin technique works and think there may be cases for which it's a better fit. In the long term, all of this will presumably be changing under Perl 6; perhaps the roles mechanism will provide a unified way of combining behaviors, but I haven't fully wrapped my head around its day-to-day implications. Further Work
I'd particularly like to make the method definition more transparent, so that you didn't have to explicitly fill @MIXIN, but I'm wary of using source filters. Is there a way to avoid string evals here by rebinding a clone of a subroutine to a new package, so that SUPER resolution will start from that other class? (I tried using Exporter to import the methods into the target package, but SUPER still seemed to start searching from the package in which they were compiled.) If you'd like to look at some working examples of this kind of mixin code, check out Text::MicroMason and DBIx::SQLEngine::Record::Class. Feedback welcome. Janitored by Arunbear - added readmore tags In reply to Solving the SUPER problem in Mixins with String Eval by simonm
|
|