The Parametric Role of my MVC Plugin System

| 2 Comments

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.)

2 Comments

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.

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on January 14, 2011 11:54 AM.

Why You Can't Hire Great Perl Programmers was the previous entry in this blog.

How to Identify a Good Perl Programmer is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Powered by the Perl programming language

what is programming?