Day the Eight: Customising Messages

Error Messages

Top Close Open

Yesterday we looked at the error handling facilities provided by Badger::Base. Here's a quick recap showing the HAL module with an an open_pod_bay_doors() method that raises an error.

package HAL;
use base 'Badger::Base';

sub open_pod_bay_doors {
    my $self = shift;
    $self->error("I'm sorry Dave, I'm afraid I can't do that.");
}

1;

Providing consistent and useful error messages is an important part of making your code user friendly. However, it's hard to keep control over this aspect when error messages are dotted all over your module(s).

A better approach is to define all your error messages up at the top of your module in a $MESSAGES hash. To make them more re-usable, you can specify them using printf()-like format strings. Here's an example.

package HAL;
use base 'Badger::Base';

our $MESSAGES = {
    sorry     => "I'm sorry Dave, I'm afraid I can't do that.",
    important => "This %s is too important for me to allow you to %s it.",
    missing   => "Without your %s, Dave, you're going to find that rather difficult."
};

1;

Now instead of calling the error() method, we can call the error_msg() to use one of the messages defined in $MESSAGES. The name of the message format is passed as the first argument, followed by any additional parameters that are substituted into the message using xprintf(), an enhanced version of Perl's inbuilt sprintf() function.

sub open_pod_bay_doors {
    my $self = shift;
    $self->error_msg('sorry');
}

Now when we call the open_pod_bay_doors() method like so:

use HAL;
my $hal = HAL->new;
$hal->open_pod_bay_doors;

We get an error thrown like this:

hal error - I'm sorry Dave, I'm afraid I can't do that.

One immediate benefit is that you can easily re-use messages using a quick and simple syntax.

# throws an error: 
#   Without your helmet, Dave, you're going to find that rather difficult.

sub leave_spacecraft {
    my $self   = shift;
    my $helmet = shift
        || $self->error_msg( missing => 'helmet' );

    # ...do something...
}

# throws an error: 
#   Without your iPod, Dave, you're going to find that rather difficult.

sub listen_to_music {
    my $self   = shift;
    my $ipod   = shift
        || $self->error_msg( missing => 'iPod' );

    # ...do something...
}

It follows the principle of Don't Repeat Yourself (DRY). You don't have to re-type (or cut and paste) the same (or similar) message all over your module. You define it in one place where it's easy to find and simple to change. Also, by collecting all the error messages in one place, it's a lot easier to see if they're all "playing along nicely together". That is, they're all using similar terminology, grammar, and so on. That's another important principle in software engineering: Separation Of Concerns (SOC). Put similar things in once place.

Speaking in the Local Vernacular

Top Close Open

Another benefit of having all your messages defined in one place is that you can easily change them. And we're not just talking about editing them with your favourite text editor.

Say you're writing a program for a pirate (of the nautical kind) and you would like to customise the error messages generated by the HAL module. Well that's easy - just change the formats defined in the $MESSAGES hash array.

use HAL;
$HAL::MESSAGES->{ missing } = "Avast! Ye be missin' yer %s.  Arrrr!";
$HAL::MESSAGES->{ sorry   } = "Walk the plank, ye old scurvy dog!";

my $hal = HAL->new;
$hal->open_pod_bay_doors;

Now any exceptions throw will have a suitably naughtical theme.

hal error - Walk the plank, ye old scurvy dog!

This is, of course, a rather silly example. But in the Real World™ you might be providing localised translations of messages, displaying messages in HTML, or customising them in some other interesting way.

Inheriting Messages

Top Close Open

One drawback to the previous approach is that it's a little invasive to go messing around with the original messages in the HAL class. Those changes will affect all the instances of HAL in your program. Now that might be what you want - in which case, go right ahead. But if you would rather keep the changes isolated then you can create a subclass of HAL which defines some new $MESSAGES.

use HAL::Pirate;
use base 'HAL';

our $MESSAGES = {
    missing   => "Avast! Ye be missin' yer %s.  Arrrr!",
    sorry     => "Walk the plank, ye old scurvy dog!",
};

1;

The HAL::Pirate module will now use its own $MESSAGES in preference to those defined in the HAL base class. Any that it doesn't define (like important) will be "inherited" from the base class. So now with little effort, we've got a new module with custom messages without affecting the original HAL module in any way.

Hal::Pirate->new->open_pod_bay_doors;

The error thrown looks like this. Notice how we've got the new message format and a new exception type for the subclass module.

hal.pirate error - Walk the plank, ye old scurvy dog!

Changing the Error Type

Top Close Open

One final thing we'll look at is the $THROWS package variable. This defines the default exception type for a module. You can set it to something other than the default, which is the lower case, dotted representation of the class name (e.g. HAL::Pirate throws hal.pirate errors).

package HAL::Pirate; 
use base 'HAL';

our $THROWS   = "space.pirate";
our $MESSAGES = {
    # ...as before...
}

1;

This module will now throw space.pirate errors.

space.pirate error - Walk the plan, ye old scurvy dog!
Fork Me on Github