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' );

