Perils of Plugins

This is a very old article. It has been imported from older blogging software, and the formatting, images, etc may have been lost. Some links may be broken. Some of the information may no longer be correct. Opinions expressed in this article may no longer be held.

Plugin-based architectures can be a bad idea.

Not always. In user-facing applications, where the list of installed and enabled plugins is clear, then plugins are often a good thing. This article is concerned not with end-user facing applications, but with libraries. Libraries that allow their functionality to be extended through plugins. In particular, libraries that automatically detect and load all installed plugins.

Plugins aren’t always obviously plugins. In this article, I’m defining a plugin as a software module that adds additional functionality or modifies the externally observable behaviour of the existing functionality of the core piece of software. Call it a “plugin” or an “optional dependency” – it’s the same thing.

Here’s a simple hypothetical example:

package Postcode;

our $AUTHORITY = ‘local:ALICE’;
our $VERSION = ‘1.0’;

use Modern::Perl;
use Carp qw( confess );
use Class::Load qw( try_load_class is_class_loaded );

use constant {
IDX_COUNTRY => 0,
IDX_POSTAL_CODE => 1,
NEXT_IDX => 2,
};

sub new
{
my ($class, $country, $postal_code) = @_;

# $country should be an upper-case ISO 3166 alpha-2 code
$country = uc $country;
confess “$country not a valid country identifier”
unless $country =~ /^[A-Z]{2}$/;

unless ($class =~ /::[A-Z]{2}$/)
{
my $specific_class = join ‘::’ => ($class, $country);
try_load_class($specific_class);
return $specific_class->new($country, $postal_code)
if is_class_loaded($specific_class);
}

return bless [$country, $postal_code] => $class;
}

sub country
{
my $self = shift;
$self->[ $self->IDX_COUNTRY ];
}

sub postal_code
{
my $self = shift;
uc $self->[ $self->IDX_POSTAL_CODE ];
}

1;

Hopefully what the above code does should be immediately apparent. You can construct postcode objects like this:

my $beverley_hills = Postcode::->new(US => 90210);
my $buckingham_palace = Postcode::->new(GB => “SW1A 1AA”);

If the modules Postcode::US or Postcode::GB are installed, then locale-specific objects will be constructed which may provide extra functionality like $beverley_hills->get_state; otherwise generic Postcode objects will be constructed. Here’s an example locale-specific plugin…

package Postcode::GB;

our $AUTHORITY = ‘local:ALICE’;
our $VERSION = ‘1.0’;

use Modern::Perl;
use Carp qw( confess );
use base ‘Postcode’;

sub new
{
my $self = shift->SUPER::new(@_);

# Canonicalise whitespace
$self->[ $self->IDX_POSTAL_CODE ] =~ s{\s}{}g;
$self->[ $self->IDX_POSTAL_CODE ] =~ s{(^.+)(…)$}{$1 $2}g;

return $self;
}

# XXX: this regexp doesn’t cover s