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.)
What is the value of
with 'MyApp::Role::Activates', { namespaces => [ 'MyApp::Plugin' ] };
compared to
with 'MyApp::Role::Activates';
sub activates_namespaces { [ 'MyApp::Plugin' ] };
and have MyApp::Role::Activates call $self->activates_namespaces to discover the namespaces to use?
This is not a trick question. I do understand some places where parametric roles do have clear advantages, I just don't see that in this case. I could even argue that the use of a runtime method call would provide an extra hook for future expansion.
Thanks
That's a good question. My answer is "It depends on your philosophy". The method approach would have worked fine as well, though I tend to prefer the parametric approach because it resembles the pattern where you provide an object all of its necessary instance data through the constructor and then never use mutators to change its state.
Moose tends to encourage that approach.
If you ever use the ideas of dependency injection and inversion of control (and despite their overly-complex names, the ideas make sense), parameterization can be easier to manage than subclassing to get different behavior. In this case, it's probably not necessary...
... but I've had a lot of good design experiences this way.