I recently had the joyous experience of porting some code to use proper Object Oriented Exceptions, and found a few niggles in my experience.
Exception::Class is a great module, and in terms of an Exception base class does lots of the things I want an exception module to do.
However, it has one and only one really big problem from my perspective, and that is, by default, its extensibility is a bit limited.
It appears to be highly targeted for its in-line declarations at import(), as follows:
use Exception::Class (
'MyException',
'AnotherException' => { isa => 'MyException' },
'YetAnotherException' => {
isa => 'AnotherException',
description => 'These exceptions are related to IPC'
},
'ExceptionWithFields' => {
isa => 'YetAnotherException',
fields => [ 'grandiosity', 'quixotic' ],
alias => 'throw_fields',
},
);
This is handy, For the simple case. But it doesn't do you a whole bunch of favours. Adding custom methods is a bane, and there's no field validation/processing support.
The best alternative to getting custom methods is Exception::Class::Nested which lets you do this:
use Exception::Class::Nested (
'MyException' => {
description => 'This is mine!',
'YetAnotherException' => {
description => 'These exceptions are related to IPC',
'ExceptionWithFields' => {
fields => [ 'grandiosity', 'quixotic' ],
alias => 'throw_fields',
full_message => sub {
my $self = shift;
my $msg = $self->message;
$msg .= " and grandiosity was " . $self->grandiosity;
return $msg;
}
}
}
},
);
This is loads more practical, merely by eliminating the isa => stuff and adding of custom methods, but it still lacks many things in extensibility. No Type checking, no parameter processing, and worst of all, no apparently logical path to avoid clobbering parent methods ( I'm entirely assuming the ->SUPER:: stuff works, but I dislike that peskyness with a passion ). And last but not least, that module won't even install or pass its own tests.
So, you find yourself to this sort of thing:
use strict;
use warnings;
package ExceptionWithFields;
use base 'YetAnotherException';
# Every time I have to do this, I forget how to do it
# which is especially annoying as its not documented anywhere
# and Exception::Class bolts it on to its generated exceptions during ->import()
# so the method is nowhere to be found in Exception::Class::Base 's code
# or its inheritance hierarchy.
# the inner guts of it are hidden away in Exception::Class::_make_subclass
sub Fields {
# return an array of field names or they won't get populated.
return ('grandiosity', 'quixotic');
}
# yes, you have to write your own accessors
# Exception::Class->import() generates these accessors manually.
sub grandiosity {
my ( $self ) = shift;
return $self->{grandiosity};
}
sub quixotic {
my ( $self ) = shift;
return $self->{quixotic};
}
sub full_message {
my $self = shift;
my $msg = $self->message;
$msg .= " and grandiosity was " . $self->grandiosity;
return $msg;
}
1;
YUCK!. . That's an awfully lot of nasty boilerplate :(.
This is only a simple example, so you can see how it'd get more complicated with more advanced things, I don't even want to contemplate how to handle parameter coercion/processing.
So, lets Moose this thing up!
I'm addicted to this Moose thing.
Moose probably makes Exception classes overweight, but considering how short lived they are, in many cases it doesn't really matter.
Unfortunately for us, Exception::Class uses some other weird thing which makes bolting stuff on to it a bit harder.
But fortunately, there is MooseX::NonMoose which makes this mostly painless.
use strict;
use warnings;
package MyException;
use Moose;
use MooseX::NonMoose;
use namespace::autoclean;
extends qw(Exception::Class::Base);
# This method is needed to delete things which are supposed to be handled by Moose
# so they don't get passed to the parent constructor, because excess args cause it to fail -_-
sub FOREIGNBUILDARGS {
my ( $class, %args ) = @_;
for ( $class->meta->get_attribute_list ) {
delete $args{$_};
}
return %args;
}
# Handy addition for giving back traces to user-land.
around show_trace => sub {
my ( $orig, $class, @rest ) = @_;
return 1 if exists $ENV{MYEXCEPTION_STACKTRACE} and $ENV{MYEXCEPTION_STACKTRACE};
return $class->$orig(@rest);
};
__PACKAGE__->meta->make_immutable;
Yay. Suddenly we have something Moose friendly that JustWorks as we want it to. And we've already added functionality by making all our children's stack-traces forced on by an ENV option, but otherwise behave as usual.
Now for the derivative classes
use strict; use warnings; package YetAnotherException; use Moose; use namespace::autoclean; extends 'MyException'; __PACKAGE__->meta->make_immutable(); 1;
use strict;
use warnings;
package ExceptionWithFields;
use Moose;
extends 'YetAnotherException';
use namespace::autoclean;
has 'grandiosity' => ( isa => 'Str', is => 'ro', required => 1 );
has 'quixotic' => ( isa => 'Str', is => 'ro' , required => 1 );
# Now with inheritable message code =)
around full_message => sub {
my ( $orig, $self , @args ) = @_;
my $msg = $self->$orig( @args );
$msg .= " and grandiosity was " . $self->grandiosity;
return $msg;
};
# Stick some lines *after* the stacktrace =D
around as_string => sub {
my ( $orig, $self , @args ) = @_;
my $msg = $self->$orig( @args );
$msg .= "\n\n Please refer to the ExceptionWithFields Manual for more information";
return $msg;
}
__PACKAGE__->meta->make_immutable();
WAAAY More fun. Waaay Less headaches. Moose++
#!/usr/bin/perl
use ExceptionWithFields;
ExceptionWithFields->throw(
message => "This is a test",
grandiosity => "This is grand!",
quixotic => "Very!",
);

5 comments: