Object::Trampoline Why having not the object you want is just what you need. Steven Lembark Workhorse Computing [email protected]
Jul 16, 2015
Object::Trampoline
Why having not the object you want is just what you need.
Steven LembarkWorkhorse [email protected]
We've all been there
Starting the web server crashes your database.
Testing requires working handles for services you don't test.
Compiling error messages in 15 languages you never use.
Patterns: Ways to Scratch an Itch
Language-specific, idiomatic solutions.
Meta-data about language strengths, limitations.
Example: C patterns are about pointers and string handling.
Perl patterns here: objects, dispatch, and stack manglement.
Pattern: “Lazy Initialization”
Separate “construction” and “initialization”.
You only need to construct an object to dispatch with it.
Delay initialization until methods are called.
Common way: If-logic
Construct a naked object.
Check object contents within every method, overload, ...
Works until someone forgets to check...
… or benchmarks the overhead of checking.
Other ways:
Use a factory class to construct a new object every time.Hard to maintain state across calls.
Use a cached connection (e.g., DBI::connect_cached).Expensive: check what you know to be true every time.
Not lazy or impatient.
Pattern: Trampoline Class
Simplest approach: co-operating classes with re-bless.
Construct the object in one class.
Bless it into the “bounce class”.
Bounce class initializes the object.
Re-bless the object where it belongs.
Warning:Warning:
Code shown here containsCode shown here containsgraphicgraphic AUTOLOAD's, AUTOLOAD's,unfiltered bless,unfiltered bless,& hardwired stack.& hardwired stack.
Parenthetical Parenthetical discretiondiscretion is advised. is advised.
Example: Multi-stage initialization
package Foo;package Foo;
## new calls the constructor, re-blesses the new calls the constructor, re-blesses the## object then stores the initialize data. object then stores the initialize data.
sub newsub new{{
my $obj = my $obj = bless &construct,bless &construct, ' 'Foo::BounceFoo::Bounce';';
$initz{ refaddr $obj } = [ @_ ];$initz{ refaddr $obj } = [ @_ ];
$obj$obj}}
Example: Multi-stage initialization
package Foo::Bounce;package Foo::Bounce;AUTOLOADAUTOLOAD{{
......# # reset the class, lazy initializereset the class, lazy initialize
bless $obj, 'Foo';bless $obj, 'Foo';
my $argzmy $argz = delete $initz{ refaddr $obj };= delete $initz{ refaddr $obj };
$obj->initialize( @$argz );$obj->initialize( @$argz );
goto &{ $object->can( $method ) }goto &{ $object->can( $method ) }}}
Problem: Co-operating classes require classes.
Hard to add after the fact.
What if you can't re-write the class?
Wrapping every single class is not an option.
A generic solution is the only reasonable fix.
Another Pattern: “Flyweight Object”
Cheap to construct.
Stand-in in for a bulky, expensive object.
Example: Surrogate keys, Unix file descriptors.
Like references or pointers, managed by user code.
Flyweight: Trampoline Object
Replaces itself with another object.
Temporary stand-in for the “real” object.
Only behavior: creating a new object and re-dispatching.
Pass trough the class/object once.
How is it done?
Quite easily:AUTOLOAD's can intercept method calls cleanly.
References modify the object in place.
goto &sub replaces a call on the stack.
Result: Method re-dispatched with a newly-minted object.
No if-logic, no re-construction, no key-magic.
Click to add code
The “Foo::Bar” class...
my $obj = my $obj = Foo::BarFoo::Bar->foobar->foobar((
qw( bim bam )qw( bim bam )););
The “Foo::Bar” class...
… becomes an argument.
my $obj = my $obj = Foo::BarFoo::Bar->frob->frob((
qw( bim bam )qw( bim bam )););
my $obj = my $obj = Object::TrampolineObject::Trampoline->frob->frob((
qw( qw( Foo::BarFoo::Bar bim bam ) bim bam )););
Click to add code
What you knead
Object::Trampoline is nothing but an AUTOLOAD.
Blesses a subref into Object::Trampoline::Bounce.
Yes, Virginia, Perl can bless something other than a hash.
Construct a bouncing object
package Object::Trampoline;package Object::Trampoline;
AUTOLOADAUTOLOAD{{
shift;shift;
my ( $protomy ( $proto, @argz ) = @_;, @argz ) = @_;my $methodmy $method = ( split /::/, $AUTOLOAD )[ -1 ];= ( split /::/, $AUTOLOAD )[ -1 ];
my $objmy $obj = = sub { $proto->$method( @argz ) }sub { $proto->$method( @argz ) };;bless $obj, 'bless $obj, 'Object::Trampoline::BounceObject::Trampoline::Bounce''
}}
Bouncing the object
Call the constructor: $_[0] = $_[0]->();
At this point it is no longer a Trampoline object.
AUTOLOAD deals with the method call.
Needs a stub DESTROY to avoid creating the object.
And, Viola!, the object you wanted.
AUTOLOADAUTOLOAD{{
# # assign $_[0] replaces caller's object.assign $_[0] replaces caller's object.
$_[0] = $_[0]->();$_[0] = $_[0]->();my $class = blessed $_[0];my $class = blessed $_[0];my $method = ( split /::/, $AUTOLOAD )[ -1 ];my $method = ( split /::/, $AUTOLOAD )[ -1 ];
local $a = $class->can( $method ) ) and goto &$a;local $a = $class->can( $method ) ) and goto &$a;
my $obj = shift;my $obj = shift;$obj->$method( @_ )$obj->$method( @_ )
}}DESTROY {}DESTROY {}
Override UNIVERSAL
All classes inherit DOES, VERSION, isa, can.
This means that the AUTOLOAD will not intercept them.
Fix: Overload UNIVERSAL methods to use AUTOLOAD.
Override UNIVERSAL
for my $name ( keys %{ for my $name ( keys %{ $::{ 'UNIVERSAL::' }$::{ 'UNIVERSAL::' } } ) } ){{ *{ qualify_to_ref $name }*{ qualify_to_ref $name } = sub= sub {{ $AUTOLOAD = $name;$AUTOLOAD = $name; goto &AUTOLOADgoto &AUTOLOAD };};}}
Extra-Bouncy: Object::Trampoline::Use
There are times when using the module is important.
Object::Trampoline::Use does a string eval.
Pushes a “use $package” into the caller's class.
Accommodates side-effects of import in the correct module.
Lazy “use”
AUTOLOADAUTOLOAD{{
......my $sub =my $sub =subsub{{
eval "package $caller; use $class” or croak ... or croak ...$class->$method( @argz )$class->$method( @argz )
};};
bless $sub, 'bless $sub, 'Object::Trampoline::BounceObject::Trampoline::Bounce''}}
Hence a subref:
Object::Trampoline::Bounce has no idea what the sub does.
Encapsulation gives a better division of labor:
One class knows what gets done, O::T::B does it reliably.
Any class that wants to can use O::T::B.
Feeding the Industrial Revolution
Shifted off the stack, lexical copies are replaced in-place.
Trivial to use Trampolines as factory classes.
Annoying if you don't plan correctly.
Shifting without factories
Data::Alias simplifies shifting objects off the stack:alias my $obj = shift;
The “alias” leaves $obj as a reference to the original.
After that trampolines will do the right thing.
Example: Dealing with handles.
O::T originally developed for a call-center system.DBI to query pending trouble tickets.
Asterisk to dial out to the group assigned to handle a ticket.
SIP to originate the calls.
Berkeley DB handle to query Asterisk status.
Testing complicated by unnecessary servers.
Fix was trampolines.
Sharing is caring
The server handles were installed from a common module.
Singleton handles update in place on use.
Testing only requires the handles actually exercised.
Sharing a handle
my %handlz =my %handlz =((
dbhdbh => Object::Trampoline->new( @dbi_argz ),=> Object::Trampoline->new( @dbi_argz ),sip_hsip_h => Object::Trampoline->new( @sip_argz ),=> Object::Trampoline->new( @sip_argz ),......
););sub importsub import{{
my $callermy $caller = caller;= caller;for my $name ( @_ )for my $name ( @_ ){{
*{ qualify_to_ref $name, $caller }*{ qualify_to_ref $name, $caller } = \$handlz{ $name }; = \$handlz{ $name };}}returnreturn
}}
Better Errors
Bad Error: “Error: File not found.”
Also Bad: “Error: xyz.config not found”
Fix: pre-check the files, reporting the working directory.
Add error messages with full paths, failure reasons.
Fix: codref blessed into O::T::Bounce.
Finding sanity
packagepackage Sane::FooSane::Foosub newsub new{{
my $pathmy $path = shift;= shift;bless subbless sub{{
my $cwdmy $cwd = getcwd;= getcwd;-e $path-e $path or die "Non-existant: '$path' ($cwd)";or die "Non-existant: '$path' ($cwd)";-r _-r _ or die "Non-readable: '$path' ($cwd)";or die "Non-readable: '$path' ($cwd)";......Foo->new( $path )Foo->new( $path )
}, }, 'Object::Trampoline::Bounce''Object::Trampoline::Bounce'
}}
Automatic checking
Sane::Foo flyweight does the checking automatically.
Object::Trampoline::Bounce executes the object.
No outside data required: Just the closure.
Returns a Foo object after pre-check.
Trivial to add $$ check for crossing process boundarys.
Sufficiently developed technology...
Misguided magic:Anyone who violates encapsulation gets what they deserve...
… or the objects have to be tied and overloaded.
Accidental factory objects.
Relying too early on import side-effects w/ O::T::Use.