Symbol Table Manipulation
by Phil Crow
|
Pages: 1, 2
Classes from Sheer Magic
The previous example demonstrated how to make symbol table entries whenever
you want. These can save typing and/or make things more readable. The standard
module English uses this
technique to give meaningful English names to the standard punctuation
variables (like $_). You want more, though. You want to build
classes out of thin air during run time.
The key to fabricating classes is to realize that a class is just a package
and a package is really just a symbol table (more or less). That, and the fact
that symbol tables autovivify, is all you need to carry off hugely helpful
deceptions like Class::DBI::mysql.
What use really does
This subsection explains how to pass data during a use
statement. If you already understand the import subroutine, feel
free to skip to the next section.
When you use a module in Perl, you can provide information for that module
to use during loading. While Class::DBI::mysql waits for you to
call routines before setting up classes, Class::Colon does it
during loading by implementing an import method.
Whenever someone uses your module, Perl calls its
import method (if it has one). import receives the
name of the class the caller used, plus all of the arguments provided by the
caller.
In the checkbook example above, the caller used Class::Colon
with this statement:
use Class::Colon Trans => [ qw(
status type date=Date amount desc category memo
) ];
The import method of the Class::Colon package
receives the following as a result:
- The string
Class::Colon. - A list with two elements. First, the string
Trans. Second, a reference to the array which lists the fields.
The top of the import routine stores these as shown below.
Inserting into non-existent symbol tables
The main magic of Class::Colon happens in the
import routine. Here's how that looks:
sub import {
my $class = shift;
my %fakes = @_;
foreach my $fake (keys %fakes) {
no strict;
*{"$fake\::NEW"} = sub { return bless {}, shift; };
foreach my $proxy_method qw(
read_file read_handle objectify delim
write_file write_handle stringify
) {
my $proxy_name = $fake . "::" . uc $proxy_method;
my $real_name = $class . "::" . $proxy_method;
*{"$proxy_name"} = \&{"$real_name"};
}
my @attributes;
foreach my $col (@{$fakes{$fake}}) {
my ($name, $type, $constructor) = split /=/, $col;
*{"$fake\::$name"} = _make_accessor($name, $type, $constructor);
push @attributes, $name;
}
$simulated_classes{$fake} = {ATTRS => \@attributes, DELIM => ':'};
}
}
After shifting the arguments into meaningful variable names, the main loop
walks through each requested class (the list of fakes). Inside the loop it
disables strict, because the necessary uses of so many symbolic
references would upset it.
There are four steps in the fabrication of each class:
- Make the constructor
- Make the class methods
- Make the accessor methods
- Store the attribute names in order
The constructor is about as simple as possible and the same for every
fabricated class. It returns a hash reference blessed into the requested class.
The cool thing is that you can insert code into a symbol table that doesn't
exist in advance. This constructor will be NEW. (By convention,
Class::Colon uses uppercase names for its methods to avoid name
collisions with the user's fields).
This code requires a little bit of careful quoting. Saying
*{"$fake\::NEW"} tells Perl to make an entry in the new package's
symbol table under NEW. The backslash suppresses variable
interpolation. While $fake needs interpolation, interpolating
$fake::NEW would just yield undef, because this is
its first definition here.
Perl has already done the hard part by the time it stores the constructor. It has brought the package into existence. Now it's just a matter of making some aliases.
For each provided method, the code makes an entry in the symbol table of the
fabricated class. Those entries point to the methods of the
Class::Colon package, which serve as permanent shared delegates
for all fabricated classes.
Similarly, it builds an accessor for each attribute supplied by the caller
in the use statement. These routines require a bit of
customization to look up the proper attribute name and to deal with object
construction. Hence, there is a small routine called
_make_accessor which returns the proper closure for each
accessor.
Finally, it makes an entry for the new class in the master list of simulated
classes. This allows easy lookup by name when calling class methods through the
fabricated names. Note that there is nothing in the import routine
that limits the caller to one invocation. Further use statements
can bring additional classes to life. Alternatively, the caller can request
several new classes with a single use statement by including
multiple hash keys.
In the standard case, _make_accessor works like this:
sub _make_accessor {
my $attribute = shift;
return sub {
my $self = shift;
my $new_val = shift;
$self->{$attribute} = $new_val if defined $new_val;
return $self->{$attribute};
};
}
The actual routine is a bit more complex, so it can handle construction of
attributes which are objects. Note that the value of $attribute,
which is in scope when the closure is created, will be kept with the sub and
used whenever it is called. The actual code is a fairly standard Perl dual-use
accessor. It assigns a new value to the attribute if the caller has passed it
in. It always returns the value of the attribute.
What Class::Colon provides
Just for sake of completeness, here is how Class::Colon turns a
string into a set of objects. Note the heavy use of methods through their
previously-entered symbol table names.
sub objectify {
my $class = shift;
my $string = shift;
my $config = $simulated_classes{$class};
my $col_list = $config->{ATTRS};
my $new_object = $class->NEW();
my @cols = split /$config->{DELIM}/, $string;
foreach my $i (0 .. @cols - 1) {
my $method = $col_list->[$i];
$new_object->$method($cols[$i]);
}
return $new_object;
}
|
Related Reading
Learning Perl Objects, References, and Modules |
All fabricated classes share this method (and the other class methods of
Class::Colon).
Recall that NEW returns a blessed hash reference with nothing
in it. In objectify, the loop fills in the attributes by calling
their accessors. This ensures the proper construction of any object attributes.
Callers access objectify indirectly when they call
READ_FILE and its cousins. They can also use it directly through
its OBJECTIFY alias.
Summary
By making entries into symbol tables, you can create aliases for data that is
hard to name. Further, you can create new symbol tables simply by referring to
them. This allows you to build classes on the fly. Modules like
Class::DBI::mysql and Class::Colon do this to provide
classes representing tabular data.
There are other uses of these techniques. For example, Memoize wraps an original function with a cache scheme, storing the wrapped version in place of the original in the caller's own symbol table. For functions which return the same result whenever the arguments are the same, this can save time. Exporter does even more sophisticated work to pollute the caller's symbol table with symbols from a used package. At heart, these schemes are similar to the one shown above. By carefully performing symbol table manipulations in modules, you can often greatly simplify an API, making client code easier to read, write, and maintain.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 1 of 1.
- Mock objects -is Mock page context and request obj r not interdependent
2005-03-22 04:35:08 shivaranjani [Reply]
Hi all,
I am doing Junit testing.am writing test cases using mockobjects for handling Http Servlet type of paerameters.If i access mockHttpRequest object from mockPageContext then the request object is returning null.for this I used the following code.I know that page context and request objects are interdependable.
MockPageContext pageContext=new MockPageContext();
MockHttpServletRequest request=(MockHttpServletRequest)
pageContext.getRequest();
Validate.notNull(request);
why it is so?If anybody knows plz write me back



