On a recent client project, I added a plugin system to the business logic. Whenever certain operations occur (a new user creates an account, a password changes, et cetera), the system needs to be able to perform arbitrary external operations. I know what some of these operations are (logging, updating external authentication systems), but the client will add more to the system in the future.

After some design work, I settled on a role, here called MyApp::Role::Activates . It applies to a model class with the code:

package MyApp::Schema::ResultSet::User; use Moose; use Modern::Perl; use namespace::autoclean; extends 'DBIx::Class::ResultSet'; with 'MyApp::Role::Activates', { namespaces => [ 'MyApp::Plugin' ] }; ...

This parametric role allows the use of specific namespaces in which to find its plugins. That customization gives a genericity which I can reuse and exploit in the future, if not in this project. The role provides a single method. For example, when validating the parameters of a new user and creating that new user:

sub validate_and_create { my ($class, $args) = @_; my $params = $class->validate_params( $args ); my $user = $class->create( $params ); $class->activate_plugins( user_created => $user ); return $user; }

All I had to do to add this event system to my model was to add this role to each model class and figure out the name of the event and the right arguments to that event. The role itself is very simple:

package MyApp::Role::Activate; use strict; use warnings; use Try::Tiny; use namespace::autoclean; use Module::Pluggable::Object; use MooseX::Role::Parameterized; parameter 'namespaces', isa => 'ArrayRef', required => 1; role { my $p = shift; my $namespaces = $p->namespaces; has 'plugins', is => 'ro', isa => 'ArrayRef', lazy_build => 1; method _build_plugins => sub { return [ Module::Pluggable::Object->new( instantiate => 'new', search_path => $namespaces, )->plugins ]; }; method activate_plugins => sub { my ($self, $method, @args) = @_; for my $plugin (@{ $self->plugins }) { try { $plugin->$method( @args ) }; } }; };

This code is immensely simpler thanks to Module::Pluggable. The laziness of the plugins attribute means that it's cheap to apply this role to classes without having to scour the filesystem for plugins until they're necessary.

The abstraction of this role also allows me to produce an asynchronous plugin system if necessary. Right now, plugin activation always takes place synchronously. Only this code needs to change to change that.

The final part of the system is a role which represents a plugin:

package MyApp::Role::Plugin; use Moose::Role; sub user_created {} sub user_password_changed {} ... 1;

All valid plugins should perform this role. They can choose not to implement some or all of the methods of this role, in which case they will do nothing (not even crash) for a given action. Documenting how to write a plugin is a case of explaining a bit of boilerplate ("Here's how to write a Moose class which consumes this role") and listing the appropriate plugin methods and their arguments.

The result is a system where my client can add arbitrary features without my help. There are ways in which to make the system more robust if necessary, but for now it's a fine and simple and—in some ways—elegant system.

(I could have gone further in the design by adding annotations to methods in the model which should throw events, but I appreciate the simplicity of "A plugin activation is just a method call". Sometimes it's better not to introduce syntax, as tempting as creating declarative decorations may be, for the sake of not introducing extra work.)