13.5.3. Discussion
The
Class::Struct::struct function builds struct-like
classes on the fly. It creates a class of the name given in the first
argument, complete with a constructor named new
and per-field accessor methods.
In the structure layout definition, the keys are the names of the
fields and the values are the data type. This type can be one of the
three base types: '$' for scalars,
'@' for arrays, and '%' for
hashes. Each accessor method can be invoked without arguments to
fetch the current value, or with an argument to set the value. For a
field whose type is an array or hash, a zero-argument method
invocation returns a reference to the entire array or hash, a
one-argument invocation retrieves the value at that
subscript,[24] and a two-argument invocation sets the
value at that subscript.
[24]Unless it's a reference, in which case it
uses that as the new aggregate, with type
checking.
The type can even be the name of another named structure—or any
class, for that matter. Because a class constructor doesn't have to
be named new, if a component of your class is
another object class, you'll have to invoke that named constructor
yourself.
use Class::Struct;
struct Person => {name => '$', age => '$'};
struct Family => {head => 'Person', address => '$', members => '@'};
$folks = Family->new( );
$folks->head($dad = Person->new);
$dad->name("John");
$dad->age(34);
printf("%s's age is %d\n", $folks->head->name, $folks->head->age);
You can pass the constructors created by Class::Struct initializer
pairs:
$dad = Person->new(name => "John", age => 34);
$folks->head($dad);
Internally, the class is implemented using a hash, just as most
classes are. This makes your code easy to debug and manipulate.
Consider the effect of printing out a structure in the debugger, for
example. If you use the Perl debugger's x command to dump out the
$folks object you've just created, you'll notice
something interesting:
DB<2> x $folks
0 Family=HASH(0xcc360)
'Family::address' => undef
'Family::head' => Person=HASH(0x3307e4)
'Person::age' => 34
'Person::name' => 'John'
'Family::members' => ARRAY(0xcc078)
empty array
Each hash key contains more than the just the name of the method:
that name is prefixed by the package name and a double-colon. This
convention guards against two classes in the same inheritance
hierarchy using the same slot in the object hash for different
purposes. This is a wise practice to follow for your own classes,
too. Always use the package name as part of the hash key, and you
won't have to worry about conflicting uses in subclasses.
If you'd like to impose more parameter checking on the fields'
values, supply your own version for the accessor method to override
the default version. Let's say you wanted to make sure the age value
contains only digits, and that it falls within reasonably human age
requirements. Here's how that function might be coded:
sub Person::age {
use Carp;
my ($self, $age) = @_;
if (@_ > 2) { confess "too many arguments" }
elsif (@_ = = 1) { return $self->{"Person::age"} }
elsif (@_ = = 2) {
carp "age `$age' isn't numeric" if $age !~ /^\d+/;
carp "age `$age' is unreasonable" if $age > 150;
$self->{'Person::age'} = $age;
}
}
Using the principles outlined in Recipe 12.15,
you can provide warnings only when warnings have been requested using
warnings::enabled. Once your module has registered
its package as a warnings class with use
warnings::register, you can write:
if (warnings::enabled("Person") || warnings::enabled("numeric")) {
carp "age `$age' isn't numeric" if $age !~ /^\d+/;
carp "age `$age' is unreasonable" if $age > 150;
}
You could even complain when warnings are in force, but raise an
exception if the user hadn't asked for warnings. (Don't be confused
by the pointer arrow; it's an indirect function call, not a method
invocation.)
my $gripe = warnings::enabled("Person") ? \&carp : \&croak;
$gripe->("age `$age' isn't numeric") if $age !~ /^\d+/;
$gripe->("age `$age' is unreasonable") if $age > 150;
The Class::Struct module also supports an array representation. Just
specify the fields within square brackets instead of curly ones:
struct Family => [head => 'Person', address => '$', members => '@'];
Empirical evidence suggests that selecting the array representation
instead of a hash trims between 10% and 50% off the memory
consumption of your objects, and up to 33% of the access time. The
cost is less informative debugging information and more mental
overhead when writing override functions, such as
Person::age shown earlier. Choosing an array
representation for the object would make it difficult to use
inheritance. That's not an issue here, because C-style structures
employ the much more easily understood notion of aggregation instead.
The use fields pragma provides
the speed and space of arrays with the expressiveness of hashes, and
adds compile-time checking of an object's field names.
If all fields are the same type, rather than writing it out this way:
struct Card => {
name => '$',
color => '$',
cost => '$',
type => '$',
release => '$',
text => '$',
};
you could use a map to shorten it:
struct Card => { map { $_ => '$' } qw(name color cost type release text) };
Or, if you're a C programmer who prefers to precede the field name
with its type, rather than vice versa, just reverse the order:
struct hostent => { reverse qw{
$ name
@ aliases
$ addrtype
$ length
@ addr_list
}};
You can even make aliases, in the (dubious) spirit of
#define, that allow the same field to be accessed
under multiple aliases. In C, you can say:
#define h_type h_addrtype
#define h_addr h_addr_list[0]
In Perl, you might try this:
# make (hostent object)->type( ) same as (hostent object)->addrtype( )
*hostent::type = \&hostent::addrtype;
# make (hostenv object)->addr( ) same as (hostenv object)->addr_list(0)
sub hostent::addr { shift->addr_list(0,@_) }
As you see, you can add methods to a class—or functions to a
package—simply by declaring a subroutine in the right
namespace. You don't have to be in the file defining the class,
subclass it, or do anything fancy and complicated. It might be better
to subclass it, however:
package Extra::hostent;
use Net::hostent;
@ISA = qw(hostent);
sub addr {
shift->addr_list(0,@_) }
1;
That
one's already available in the standard Net::hostent class, so you
needn't bother. Check out that module's source code as a form of
inspirational reading. We can't be held responsible for what it
inspires you to do, though.