17.7. Interface Variables
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.
Example 17-1. Variables as a module's interfacepackage 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; 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 ); |