home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Perl CookbookPerl CookbookSearch this book

13.5. Using Classes as Structs

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.



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.