# ==================================================================== # Gossamer Threads Module Library - http://gossamer-threads.com/ # # GT::Delay # Author: Jason Rhinelander # CVS Info : # $Id: Delay.pm,v 1.4 2004/01/13 01:35:15 jagerman Exp $ # # Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved. # ==================================================================== # # Description: # Generic delayed-loading module wrapper. # package GT::Delay; use strict; use Carp(); my %Delayed; sub GT::Delay { # We don't define any subroutines in GT::Delay, since even ->new should be # allowed in some circumstances. Takes three arguments - the package to load # (i.e. 'GT::SQL'), the type of blessed reference used for that object ('HASH', # 'ARRAY', and 'SCALAR' are supported), and any number of arguments to pass # into the ->new method of the package. # my ($package, $type, @args) = @_; $type ||= 'HASH'; $type eq 'HASH' || $type eq 'ARRAY' || $type eq 'SCALAR' or Carp::croak('Unknown bless type: ' . $type . '. See the GT::Delay manpage'); my $self = bless($type eq 'HASH' ? {} : $type eq 'ARRAY' ? [] : \my $foo); $Delayed{$self} = [$package, $type, \@args]; $self; } AUTOLOAD { # When a method is called we create a real object, copy it into $self, and # rebless $self into the package. This has to be done to get around a case # such as: my $foo = GT::Delay(...); my $bar = $foo; $bar->meth; # Even changing $_[0] would not affect $foo, and if $foo was used would result # in _two_ of the delayed modules. # my $self = $_[0]; my ($package, $type, $args) = @{delete $Delayed{$self}}; (my $module = $package) =~ s|::|/|g; $module .= '.pm'; require $module; my $copy = $package->new(@$args); eval { if ($type eq 'HASH') { %$self = %$copy } elsif ($type eq 'ARRAY') { @$self = @$copy } else { $$self = $$copy } }; $@ and Carp::croak("$package type does not appear to be $type. Delayed loading failed"); bless $self, ref $copy; my $method = substr($GT::Delay::AUTOLOAD, rindex($GT::Delay::AUTOLOAD, ':') + 1); if (my $subref = $self->can($method)) { goto &$subref; } elsif ($self->can('AUTOLOAD')) { shift; $self->$method(@_); } else { Carp::croak(qq|Can't locate object method "$method" via package "| . ref($self) . '"'); } } DESTROY { delete $Delayed{$_[0]} if exists $Delayed{$_[0]}; } 1; __END__ =head1 NAME GT::Delay - Generic delayed module loading =head1 SYNOPSIS use GT::Delay; my $obj = GT::Delay('GT::Foo', 'HASH', foo => "bar", bar => 12); ... # time passes without using $obj $obj->method(); =head1 DESCRIPTION This module provides a simple way to handle delayed module loading in a fairly generic way. Your object will only be a very lightweight GT::Delay object until you call a method on it, at which point the desired module will be loaded, your object will be changed into an object of the desired type. =head1 FUNCTIONS There is only one usable function provided by this module, GT::Delay() (not GT::Delay::Delay as this module attempts to leave the GT::Delay namespace as empty as possible). =head2 GT::Delay GT::Delay is used to create a new delayed object. It takes at least two arguments. The first is the package to load, such as 'GT::Foo' to require GT/Foo.pm and create a new GT::Foo object. The second is the type of blessed data structure a 'GT::Foo' object really is. This can be one of either 'HASH', 'ARRAY', or 'SCALAR'. Any additional arguments are kept and passed in as arguments to the new() method of the object when created. The object type ('HASH', 'ARRAY', or 'SCALAR') is needed is to get around a caveat of references - if $a and $b both point to the same reference, $b cannot be changed from $a - which makes it impossible to just get a new object and replace $_[0] with that object, because although that would change one of either $a or $b, it wouldn't change the other and you could easily end up with two separate objects. When a method is called, the new object is created, then copied into the original object which is then reblessed into the desired package. This doesn't change either $a or $b, but rather changes the reference they point to. You have to pass the object type because the reference must be reblessed, but the underlying data type cannot change. Unfortunately, this approach has a few caveats of its own, listed below. =head1 CAVEATS and LIMITATIONS Modules that are created by a method other than new() are not supported. Modules that use a namespace different from the module location are not supported. For example, a package Foo::Bar::Blah located in Foo/Bar.pm. If you have such a module that would benefit from delayed loading, you need to rethink your package/filename naming scheme, or not use this module. It _is_ possible to do this with a hack such as: C<$INC{'Foo/Bar/Blah.pm'} = './Foo/Bar.pm';> - but other than for testing, doing such a thing is strongly discouraged. Objects cannot have their elements directly accessed - for example, C<$obj-E{foo}>. But, since that is bad practise anyway, it isn't that much of a limitation. That said, objects _can_ be accessed directly _after_ any method has been called. Modules that store a string or integer form of $self (GT::Config does this to store object attributes) will not work, since the working object will not be the same object create a new(), but rather a copy. Modules with DESTROY methods that do things to references in $self (for example, C{foo}-E{bar}> - though C{foo}> would be safe) will most likely not work properly as the copy is not deep - i.e. references are copied as-is. Along the same lines as the previous point, the first object will be destroyed before the first method call goes through, so modules that do things (e.g. delete files, close filehandles, etc.) in DESTROY will most likely not work. Any module that doesn't fall into any of the points above will be perfectly well supported by this module. =head1 MAINTAINER Jason Rhinelander =head1 COPYRIGHT Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved. http://www.gossamer-threads.com/ =head1 VERSION Revision: $Id: Delay.pm,v 1.4 2004/01/13 01:35:15 jagerman Exp $ =cut