Приглашаем посетить
Брюсов (bryusov.lit-info.ru)

10.1 Making It Robust

Previous Table of Contents Next

10.1 Making It Robust

The more robust a program is, the less likely it is to attract attention to itself and invite maintenance. Here are some modules that can help with that task.

10.1.1 Params::Validate

People who have programmed in other procedural languages like C, Java, and Pascal often wonder where the real subroutine prototypes are in Perl. They have this nagging feeling that subroutines just aren't safe until all their arguments have been validated just as strictly as those other languages do. Params::Validate does exactly what they want, and a lot more.[1]

[1] Albeit at run time, not compile time. Sorry. Perl 6 will have complete compile-time parameter checking.

If you're going to get strict about subroutine parameter checking, you might as well name your parameters. This is a good practice anyway whenever you create a subroutine that takes (or can take) more than about three arguments (that's the point at which I get the itch to stop using positional parameters).

Say you're writing a subroutine to search text for repeated phrases. You might want to specify options such as what to do with line breaks, whether capitalization matters, or what punctuation characters to ignore. Calling this subroutine with positional parameters:


@phrases = repeated_phrases($text, 0, 1, [qw(" ' ; , !)]);

is just going to be confusing, and using symbolic constants:


@phrases = repeated_phrases($text,

                            IGNORE_LINE_BREAKS,

                            RESPECT_CAPITALIZATION,

                            [qw(" ' ; , !)]);

is not much better: You won't be told if you got the parameter order wrong, and you have to look it up each time you want to use the subroutine. Instead, write the subroutine to expect a hash of options, so you can call it like this:


@phrases = repeated_phrases($text, capitalization => 1,

                            ignore_punc => [qw(" ' ; , !)]);

Just a moment—what happened to the specification for line breaks? We no longer need it, because ignoring them is a sensible thing to do by default, and so we say so in the documentation for repeated_phrases(). Because ignoring is the default, we just made the parameter name capitalization instead of respect_capitalization. Now we no longer need to remember the order in which these parameters must be specified, either.

The next step up the evolutionary chain from named parameters is Params::Validate, by Dave Rolsky, and a giant step it is. The subroutine writer calls the validate() function (exported by Params::Validate) at or near the beginning of a subroutine, and passes the subroutine parameters along with a template specifying what arguments are valid.

By default validate() will throw an exception when the subroutine is called with a parameter that isn't specified in the template, but you can make parameters optional and specify defaults. The result of a successful call to validate() is the hash of parameter names and values that was input. You have complete control over what sort of validation is done; the standard options allow you to permit additional parameters or allow case-insensitive parameters or parameters with leading minus signs. Here's how to use Params::Validate on the repeated_phrases() subroutine:


use Params::Validate qw(:all);



my $text = join "", <>;

my @phrases = repeated_phrases(text           => $text,

                               capitalization => 1,

                               ignore_punc  => [qw(" ' ; , !)]

                              );



sub repeated_phrases

{

  my %param = validate(@_, {

        text           => { type => SCALAR },

        capitalization => { type => SCALAR, optional   => 1 },

        ignore_punc    => { type => ARRAYREF, optional => 1 },

        line_breaks    => { type => SCALAR, optional   => 1 }

                           });

  # ... Go on to look for repeated phrases, secure in the

  # knowledge that our parameter list is squeaky clean

}

Params::Validate can also validate positional parameters, via the exported validate_pos() function. You can deal with partially positional and partially named argument lists if you're clever enough to shift off the positional ones first. A simple case of this is an object or class method call; first you have to shift off the object or class before you can call validate() on the rest of @_.

There is far more that Params::Validate can do. See the module's documentation for full details.

10.1.2 Carp::Assert

A kissing cousin of Params::Validate is Carp::Assert, which enables you to make general assertions about the state of your program. (Okay, make that a kissing cousin twice removed . . . but they are still related.) Assertions are a venerable feature of "grown-up" languages and are like training wheels; you enable them for development, but turn them off in production environments where speed is an issue.

An assertion will cause an exception if a given condition is false. If you ever wrote something like:


die "This shouldn't happen!!!" if ...

then it would be more recognizable as an assertion. Carp::Assert gives you several ways of doing that:


use Carp::Assert;



sub yes_we_have_no_bananas

{

  my $bananas = shift;

  assert($bananas == 0) if DEBUG;

  print "Yes, we have no bananas\n";

}

Carp::Assert doesn't print "This shouldn't happen!!!" though. (Assertions are too staid for that sort of language.) Instead it prints "Assertion failed!" and a stack trace. If you'd like it to print out a message of your own, you can give a second argument:


assert($bananas == 0, "We DO have bananas!") if DEBUG;

and then the first line of an exception is "Assertion (We DO have bananas!) failed!"

What's the if DEBUG for? That's for improving performance when you want to disable that assertion and all the others in your code easily. Carp::Assert exports a constant, DEBUG , set to a true value. If you are satisfied that you have expunged from your code all the bugs that could fire an assertion, and you want it to be as efficient as possible in production, then you don't want Perl to spend time compiling and executing assertions.

You can prevent Perl from executing assertions by changing one word: instead of use Carp::Assert, you say no Carp::Assert. Now the assert() function becomes a function that returns immediately without doing anything. But that's still a subroutine call; if you're serious about performance you don't want even that. The purpose of saying if DEBUG is that Perl will not compile code that it can tell at compile time will never be executed, and because constants like DEBUG are defined at compile time, if DEBUG is false, then Perl won't compile the assert() statement.

Obviously, assert() is simply syntactic sugar for code that raises an exception if some condition is false. You could get the same effect with


use Carp;

use constant DEBUG => 1;

# ...

if (DEBUG)

{

  confess "We DO have bananas!" unless $bananas == 0;

}

but the verbosity of that construction is far more intrusive, and you want assertions to be as lightweight as possible.

10.1.3 Class::Contract

Damian Conway's Class::Contract (http://search.cpan.org/dist/Class-Contract/) , now maintained by C. Garrett Goebel, implements the Design by Contract approach that originated in the Eiffel world [EIFFEL03]. Successfully using this approach requires high degrees of expertise in designing and discipline in implementing object classes; the payoff is a sturdy safety net.

Design by Contract is like assertions that have been pumped full of object-oriented steroids. It says that:

  • Every method in the class shall define at least one precondition, which is a test of conditions that the method requires to be satisfied in order to do its job. Class::Contract will take care of running the precondition test every time the method is invoked before it actually does anything, and if the test fails, an exception will be thrown.

  • Every method in the class shall define at least one postcondition, which is a test of what the method is supposed to guarantee to its caller. Class::Contract will take care of running the precondition test every time the method is invoked just before it returns to its caller, and if the test fails, an exception will be thrown.

  • The class shall specify a global test or invariant, which is another test that will be run at the end of each method call and that tests that the method has not violated the integrity of the class.

As you might be able to tell, coming up with proper conditions might require considerable expertise. Class::Contract provides these capabilities and many more (e.g., conditions associated with object attributes); to do this, Conway virtually reimplemented large parts of Perl's object-oriented system (e.g., inheritance). In Class::Contract, you specify methods, attributes, and inheritance differently from the native Perl way; you use Class::Contract's own language for doing so. Here's a working, if extremely naive, example to show you what that language looks like:[2]

[2] Class::Contract is best for very large applications that won't fit in this book.

Example 10.1. BankAccount.pm Example for Class::Contract

package BankAccount;

use strict;

use warnings;

use Class::Contract qw(old);



my $INIT_RESERVES = 10_000;  # Bailey Building & Loan



contract

{

  class attr 'reserves' => 'SCALAR';

  class ctor;

    impl { ${self->reserves} = $INIT_RESERVES };

  class method 'adjust_reserves';

    pre  { defined $_[0] };

      failmsg "No amount to adjust reserves by given";

    impl { ${self->reserves} += $_[0] };

  invar { ${self->reserves} >= 0 };

    failmsg "We gone bankrupt, Maw!";

  ctor 'new';

    impl { ${self->balance} = $_[0] };

  attr balance => 'SCALAR';

  method 'get_balance';

    impl { ${self->balance} };

  method 'loan';

    pre  { defined $_[0] };

    impl { ${self->balance} += $_[0];

           self->adjust_reserves(-$_[0])

         };

    post { ${self->balance} == ${old->balance} + $_[0] };

  method 'invest';

    pre  { defined $_[0] };

    impl { ${self->balance} -= $_[0];

           self->adjust_reserves($_[0])

         };

    post { ${self->balance} == ${old->balance} - $_[0] };

};



1;

Example 10.2. Test Suite for BankAccount.pm Example for Class::Contract

#!/usr/bin/perl

use strict;

use warnings;

use BankAccount;



use Test::More tests => 7;

use Test::Exception;



BEGIN { use_ok('BankAccount') }

my $acct = BankAccount->new(1000);

is($acct->get_balance, 1000);

lives_ok { $acct->loan(5000) };

is($acct->get_balance, 6000);

dies_ok { $acct->loan };

dies_ok { $acct->invest };

throws_ok { $acct->loan(1E8) } qr/bankrupt/;

The kind of verbosity that Class::Contract results in might not be for everyone, but if you're writing that legendary air traffic control application in Perl, you'll want this kind of industrial-strength help.

10.1.4 Log::Log4perl

Michael Schilli's Log::Log4perl (http://search.cpan.org/dist/Log-Log4perl/) provides logging to multiple destinations from multiple places in a class hierarchy. Use this when you have an application that is highly object-oriented. You may have a primitive database class, for instance, for which you enable logging of its connections, and as you create subclasses that, say, know about SQL or transactions, you want to log the SQL or transaction errors, respectively.

Imagine an application using the fairly adventurous class hierarchy shown in Figure 10.1.

Figure 10.1. Log::Log4perl dispatch example class hierarchy.

graphics/10fig01.gif

Now suppose that an object in one of the leaf classes generates a logging message. Log::Log4perl allows you to attach zero or more appenders to each class; an appender is an object that is told to send log messages to a particular destination, such as a file or an e-mail recipient. Each appender is also configured to fire at a particular severity level, one of a series such as: DEBUG , INFO, WARNING , ERROR, FATAL. So if we had attached appenders at the leaf class and its ancestors, the message would percolate up through the class hierarchy of Log::Log4perl appenders, but only activating those appenders that were registered for the severity level of that message (or a lower level).

The activated appenders are denoted in bold in Figure 10.2.

Figure 10.2. Log::Log4perl dispatch example appender firing.

graphics/10fig02.gif

10.1.5 Safe.pm

Taint checking for CGI programs or other programs that operate on possibly malicious user input can go only so far. Sometimes there just isn't a useful regular expression you can use that will capture what you wanted from the user input and still be guaranteed not to result in unsafe code.

For the ultimate in code protection, Safe.pm by Malcolm Beattie and Tim Bunce (in the Perl core since version 5.004) provides an environment in which you can execute Perl code in a private sandbox. That code will only be able to read data you specifically allow it to access, only perform operations you specifically enumerate, and can't affect anything outside its sandbox except to return a result for you to use. Safe.pm is not an easy module to use, but when security is paramount it may be your best option.

    Previous Table of Contents Next