Day the Eight: Customising Messages
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.
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.
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!
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!