Sign In/My Account | View Cart  
advertisement


Listen Print Discuss

Ten Essential Development Practices
by Damian Conway | Pages: 1, 2, 3, 4, 5, 6, 7, 8

Humans simply aren't like that. Rocks almost never fall out of the sky, so humans soon conclude that they never do, and stop looking up for them. Fires rarely break out in their homes, so humans soon forget that they might, and stop testing their smoke detectors every month. In the same way, programmers inevitably abbreviate "almost never fails" to "never fails," and then simply stop checking.

That's why so very few people bother to verify their print statements:

if (!print 'Enter your name: ') {
   print {*STDLOG} warning => 'Terminal went missing!'
}

It's human nature to "trust but not verify."

Human nature is why returning an error indicator is not best practice. Errors are (supposed to be) unusual occurrences, so error markers will almost never be returned. Those tedious and ungainly checks for them will almost never do anything useful, so eventually they'll be quietly omitted. After all, leaving the tests off almost always works just fine. It's so much easier not to bother. Especially when not bothering is the default!

Don't return special error values when something goes wrong; throw an exception instead. The great advantage of exceptions is that they reverse the usual default behaviors, bringing untrapped errors to immediate and urgent attention. On the other hand, ignoring an exception requires a deliberate and conspicuous effort: you have to provide an explicit eval block to neutralize it.

The locate_and_open() subroutine would be much cleaner and more robust if the errors within it threw exceptions:

# Find and open a file by name, returning the filehandle
# or throwing an exception on failure...
sub locate_and_open {
   my ($filename) = @_;

   # Check acceptable directories in order...
   for my $dir (@DATA_DIRS) {
       my $path = "$dir/$filename";

       # If file exists in acceptable directory, open and return it...
       if (-r $path) {
           open my $fh, '<', $path
               or croak( "Located $filename at $path, but could not open");
           return $fh;
       }
   }

   # Fail if all possible locations tried without success...
   croak( "Could not locate $filename" );
}

# and later...
for my $filename (@source_files) {
   my $fh = locate_and_open($filename);
   my $head = load_header_from($fh);
   print $head;
}

Notice that the main for loop didn't change at all. The developer using locate_and_open() still assumes that nothing can go wrong. Now there's some justification for that expectation, because if anything does go wrong, the thrown exception will automatically terminate the loop.

Exceptions are a better choice even if you are the careful type who religiously checks every return value for failure:

SOURCE_FILE:
for my $filename (@source_files) {
   my $fh = locate_and_open($filename);
   next SOURCE_FILE if !defined $fh;
   my $head = load_header_from($fh);
   next SOURCE_FILE if !defined $head;
   print $head;
}

Constantly checking return values for failure clutters your code with validation statements, often greatly decreasing its readability. In contrast, exceptions allow an algorithm to be implemented without having to intersperse any error-handling infrastructure at all. You can factor the error-handling out of the code and either relegate it to after the surrounding eval, or else dispense with it entirely:

for my $filename (@directory_path) {

   # Just ignore any source files that don't load...
   eval {
       my $fh = locate_and_open($filename);
       my $head = load_header_from($fh);
       print $head;
   }
}

9. Add New Test Cases Before you Start Debugging

The first step in any debugging process is to isolate the incorrect behavior of the system, by producing the shortest demonstration of it that you reasonably can. If you're lucky, this may even have been done for you:

To: DCONWAY@cpan.org
From: sascha@perlmonks.org
Subject: Bug in inflect module

Zdravstvuite,

I have been using your Lingua::EN::Inflect module to normalize terms in a
data-mining application I am developing, but there seems to be a bug in it,
as the following example demonstrates:

   use Lingua::EN::Inflect qw( PL_N );
   print PL_N('man'), "\n";       # Prints "men", as expected
   print PL_N('woman'), "\n";     # Incorrectly prints "womans"

Once you have distilled a short working example of the bug, convert it to a series of tests, such as:

use Lingua::EN::Inflect qw( PL_N );
use Test::More qw( no_plan );
is(PL_N('man') ,  'men', 'man -> men'     );
is(PL_N('woman'), 'women', 'woman -> women' );

Pages: 1, 2, 3, 4, 5, 6, 7, 8

Next Pagearrow