Previous Page
Next Page

17.7. Interface Variables

Never make variables part of a module's interface.

Variables make highly unsatisfactory interface components. They offer no control over who accesses their values, or how those values are changed. They expose part of the module's internal state information to the client code, and they provide no easy way to later impose constraints on how that state is used or modified.

This, in turn, forces every component of the module to re-verify any interface variable whenever it's used. For example, consider the parts of a module for serializing Perl data structures[*] shown in Example 17-1.

[*] There are several such modules on the CPAN: Data::Dumper, YAML, FreezeThaw, and Storable.

Example 17-1. Variables as a module's interface
package Serialize;
use Carp;
use Readonly;
use Perl6::Export::Attrs;
use List::Util qw( max );

Readonly my $MAX_DEPTH => 100;

# Package variables that specify shared features of the module...
our $compaction = 'none';
our $depth      = $MAX_DEPTH;

# Table of compaction tools...
my %compactor = (
   # Value of      Subroutine returning
   
   # $compaction   compacted form of arg
      none     =>   sub { return shift },
      zip      =>   \&compact_with_zip,
      gzip     =>   \&compact_with_gzip,
      bz       =>   \&compact_with_bz,
      # etc.
);

# Subroutine to serialize a data structure, passed by reference...
sub freeze : Export {
    my ($data_structure_ref) = @_;

    # Check whether the $depth variable has a sensible value...
    $depth = max(0, $depth);

    
    # Perform actual serialization...
    my $frozen = _serialize($data_structure_ref);

    # Check whether the $compact variable has a sensible value...
    croak "Unknown compaction type: $compaction"
        if ! exists $compactor{$compaction};

    # Return the compacted form...
    return $compactor{$compaction}->($frozen);
}

# and elsewhere...

use Serialize qw( freeze );

$Serialize::depth      = -20;        # oops!
$Serialize::compaction = 1;           # OOPS!!!

# and later...

my $frozen_data = freeze($data_ref);      # BOOM!!!

Because the serialization depth and compaction mode are set via variables, the freeze( ) subroutine has to check those variables every time it's called. Moreover, if the variables are incorrectly set (as they are in the previous example), that fact will not be detected until freeze( ) is actually called. That might be hundreds of lines later, or in a different subroutine, or even in a different module entirely. That's going to make tracking down the source of the error very much harder.

The cleaner, safer, more future-proof alternative is to provide subroutines via which the client code can set state information, as illustrated in Example 17-2. By verifying the new state as it's set, errors such as negative depths and invalid compaction schemes will be detected and reported where and when they occur. Better still, those errors can sometimes be corrected on the fly, as the set_depth( ) subroutine demonstrates.

Example 17-2. Accessor subroutines instead of interface variables

package Serialize;
use Carp;
use Readonly;
use Perl6::Export::Attrs;

Readonly my $MAX_DEPTH => 100;

# Lexical variables that specify shared features of the module...
my $compaction = 'none'; my $depth = $MAX_DEPTH;
# Table of compaction tools...
my %compactor = (

  # Value of       Subroutine returning

  # $compaction    compacted form of arg
none => sub { return shift }, zip => \&compact_with_zip, gzip => \&compact_with_gzip, bz => \&compact_with_bz,
# etc.
);

# Accessor subroutines for state variables...
sub set_compaction { my ($new_compaction) = @_;
# Has to be a compaction type from the table...
croak "Unknown compaction type ($new_compaction)" if !exists $compactor{$new_compaction};
# If so, remember it...
$compaction = $new_compaction; return; } sub set_depth { my ($new_depth) = @_;
# Any non-negative depth is okay...
if ($new_depth >= 0) { $depth = $new_depth; }
# Any negative depth is an error, so fix it and report...
else { $depth = 0; carp "Negative depth ($new_depth) interpreted as zero"; } return; }
# Subroutine to serialize a data structure, passed by reference...
sub freeze : Export { my ($data_structure_ref) = @_; return $compactor{$compaction}->( _serialize($data_structure_ref) ); }
# and elsewhere...
use Serialize qw( freeze ); Serialize::set_depth(-20);
# Warning issued and value normalized to zero
Serialize::set_compaction(1);
# Exception thrown here

# and later...
my $frozen_data = freeze($data_ref);

Note that although subroutines are undoubtedly safer than raw package variables, you are still modifying non-local state information through them. Any change you make to a package's internal state can potentially affect every user of that package, at any point in your program.

Often, a better solution is to recast the module as a class. Then any code that needs to alter some internal configuration or state can create its own object of the class, and modify that object's internal state instead. Using that approach, the package shown in Example 17-2 would be rewritten as shown in Example 17-3.

Example 17-3. Objects instead of accessor subroutines

package Serialize;
use Class::Std;
use Carp;
{
    my %compaction_of : ATTR( default => 'none' );
    my %depth_of      : ATTR( default => 100    );

    
# Table of compaction tools...
my %compactor = (
# Value of       Subroutine returning

      # $compaction    compacted form of arg
none => sub { return shift }, zip => \&compact_with_zip, gzip => \&compact_with_gzip, bz => \&compact_with_bz, # etc. );
# Accessor subroutines for state variables...
sub set_compaction { my ($self, $new_compaction) = @_;
# Has to be a compaction type from the table...
croak "Unknown compaction type ($new_compaction)" if !exists $compactor{$new_compaction};
# If so, remember it...
$compaction_of{ident $self} = $new_compaction; return; } sub set_depth { my ($self, $new_depth) = @_;

        # Any non-negative depth is okay...
if ($new_depth >= 0) { $depth_of{ident $self} = $new_depth; }

        # Any negative depth is an error, so fix it and report...
else { $depth_of{ident $self} = 0; carp "Negative depth ($new_depth) interpreted as zero"; } return; }
# Method to serialize a data structure, passed by reference...
sub freeze { my ($self, $data_structure_ref) = @_; my $compactor = $compactor{$compaction_of{ident $self}}; return $compactor->( _serialize($data_structure_ref) ); }
# etc.
}

# and elsewhere...

# Create a new interface to the class...
use Serialize; my $serializer = Serialize->new( );
# Set up the state of that interface as required...
$serializer->set_depth(20); $serializer->set_compaction('zip');
# and later...
my $frozen_data = $serializer->freeze($data_ref);

    Previous Page
    Next Page