Beefy Boxes and Bandwidth Generously Provided by pair Networks
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
When creating an object class that will be used by different people for different purposes, I find myself drawn to solutions in which a minimal base class provides the shared behavior they all need, and a collection of subclasses provides layers of additional functionality.

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
My idea was to combine the mixins approach with a judicious spot of run-time string evals to automatically generate the inheritance tree that was needed.

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.

package Text::MicroMason::Benchmarking; use Time::HiRes 'time'; BEGIN { push @MIXIN, "#line ".__LINE__.' "'.__FILE__.'"', "", <<'/' +} sub compile { my $self = shift; my $time = time(); my $result = $self->SUPER::compile(@_); $self->show_results( $time, time() ); return $result; } / sub show_results { my ( $self, $start, $end ) = @_; warn "Compilation took " . ( $start - $stop ) . " seconds"; } 1;

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
None of this would be necessary if I used the NEXT module, which differs from SUPER in allowing method invocations to back-track through the inheritance tree and explore other branches.

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 think that this is a generally applicable technique, but I haven't yet used it in enough cases to have a sense of how to abstract the solution into a stand-alone CPAN module. Module import() time seems the right place for the inheritance and symbol table munging.

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

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post; it's "PerlMonks-approved HTML":



  • Are you posting in the right place? Check out Where do I post X? to know for sure.
  • Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
    <code> <a> <b> <big> <blockquote> <br /> <dd> <dl> <dt> <em> <font> <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <nbsp> <ol> <p> <small> <strike> <strong> <sub> <sup> <table> <td> <th> <tr> <tt> <u> <ul>
  • Snippets of code should be wrapped in <code> tags not <pre> tags. In fact, <pre> tags should generally be avoided. If they must be used, extreme care should be taken to ensure that their contents do not have long lines (<70 chars), in order to prevent horizontal scrolling (and possible janitor intervention).
  • Want more info? How to link or How to display code and escape characters are good places to start.
Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others scrutinizing the Monastery: (3)
As of 2024-03-29 06:06 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found