Post on 16-Jul-2015
transcript
Object::Trampoline
Why having not the object you want is just what you need.
Steven LembarkWorkhorse Computinglembark@wrkhors.com
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.