Building pluggable backends with Moo
I've been working through the design and implementation (using Moo) of a module (IPC::PrettyPipe) which implements some backend functionality via plugins. The nature of the module isn't that important; suffice it to say that there's a rendering backend and an execution backend.
I've previously implemented pluggable backends using Module::Pluggable and kin, but since I'm using Moo for this module I thought I'd investigate how its capabilities enabled (or constrained) pluggability.
(A point of note: Since in Moo class construction is done after the compilation stage, technically one could do all sorts of sly things, but I'd like to avoid too much sausage making. When I write "runtime", I mean after classes have been constructed.)
The main guiding principles for this module are
Ease of use: I'd like this to be accessible to the casual user. There shouldn't be a lot of setup or boilerplate code. The user shouldn't need to know what's under the hood or that it's implemented in Moo. I'd like to avoid forcing the user to create classes.
Loose coupling: The rendering and execution functionality should be easily customized. For example, I'd like to be able to use a variety of the execution pipeline modules on CPAN (such as IPC::Run, IPC::Pipeline, IPC::Exe). I'll have to write simple adaptors for those, but the IPC::PrettyPipe interface should make it easy to choose between them.
More technical requirements are:
Backends must be selectable at runtime.
The user should be able to choose a backend both during and after the construction of an IPC::PrettyPipe object.
The user must be able to change a backend after one has been selected.
There must be access to the backends to leverage any unique functionality. While IPC::PrettyPipe will provide an interface to common functionality, if a backend has special capabilities, it should be possible to access them.
What follows is a brief examination of the different approaches I explored.
Roles
Roles seem like a natural approach as they transparently
- provide direct access to the IPC::PrettyPipe object by the backend routines;
- merge unique backend functionality into the object;
For example, in addition to the run
method, IPC::Run provides the
start
, pump
, and finish
methods for interacting with the
pipeline while it is running. The adaptor for IPC::Run could compose
those methods into the derived class. If there are any extra
attributes used to customize the backend's behavior, these are
composed in as well.
Roles are typically applied during class construction, but they can also be applied dynamically at runtime. Once a role is composed into a class or object, it can't be changed, so it's not possible to meet the requirement to change a backend. It's still interesting to see how they impact the implementation, as that behavior is often not required.
"Static" Composition
Static composition forces selection of the backends during class construction, so it doesn't meet the runtime selection criteria. It does however provide a baseline to compare against for implementation and interface complexity.
Here, IPC::PrettyPipe is used as a base class and the render and execution functionality are composed into the derived class:
package MyPipe;
use Moo;
extends 'IPC::PrettyPipe';
# add the renderer
with 'IPC::PrettyPipe::Render::Template::Tiny';
# add the execution backend
with 'IPC::PrettyPipe::Execute::IPC::Run';
Dynamic Class Composition
Moo::Role (via Role::Tiny) provides the ability to dynamically create composed classes:
use Moo::Role ();
my $class = Moo::Role->create_class_with_roles( 'IPC::PrettyPipe',
'IPC::PrettyPipe::Render::Template::Tiny',
'IPC::PrettyPipe::Execute::IPC::Run' );
thus allowing runtime selection of the backends. Without some interface sugar this is too complex of an API to expose. It's just as easy to compose into an object and provide a natural interface to it, which is what the next section discusses.
Dynamic Object Composition
Role::Tiny allows one to compose a role into an existing object. This has the same drawback as dynamic class composition, but the benefit is that it's easy to implement selection of roles via methods, leading to a more traditional API:
my $pipe = IPC::PrettyPipe->new;
$pipe->renderer( 'Template::Tiny' );
$pipe->executor( 'IPC::Run' );
This is implemented in the base class as:
use Moo::Role ();
use Module::Path qw[ module_path ];
use Module::Runtime qw[ compose_module_name ];
sub renderer { $_[0]->_backend_compose( Render => $_[1] }
sub executor { $_[0]->_backend_compose( Execute => $_[1] }
# generic dynamic backend composition
sub _backend_compose {
my ( $self, $type, $req ) = @_;
my $module = compose_module_name( "IPC::PrettyPipe::$type", $req );
croak( "unknown $type ($req => $module)\n" )
unless defined module_path( $module );
Moo::Role->apply_roles_to_object( $self, $module );
return;
}
For the sake of clarity the above implementation doesn't allow specifying backends during object construction; here's the code which provides that functionality:
has _executor => (
is => 'rwp',
predicate => 1,
init_arg => 'executor',
trigger => sub { $_[0]->_backend_compose( Execute => $_[1] },
);
sub executor { $_[0]->_set__executor( $_[1] )
if defined $_[1] && ! $_[0]->_has_executor }
I'm using the trigger to ensure that composition is localized to a
single spot in the code. (The double underscore in _set__executor
is because Moo retains the leading underscore in _executor
when it
creates the private writer.)
So now, this is possible:
my $pipe = IPC::PrettyPipe->new( renderer => 'Template::Tiny',
executor => 'IPC::Run'
);
There's still no means of changing the backends, but the implementation and exposed interface are quite clean.
Method Delegation
Moving the backends into their own objects provides some flexibility by allowing them to be changed on the fly, but makes exposing any unique functionality more complex.
A complete separation of backend objects from the pipeline object might look like this:
my $pipe = IPC::PrettyPipe->new;
my $renderer = IPC::PrettyPipe::Render::Template::Tiny->new();
my $executor = IPC::PrettyPipe::Execute::IPC::Run->new();
$renderer->render( $pipe );
That's horrible, but very flexible. A cleaner approach would be for the IPC::PrettyPipe object to contain the backend objects, and use method delegatation to invoke the appropriate methods on the contained backend objects.
IPC::PrettyPipe would provide methods to specify the backends, just as in the dynamic object composition pattern:
my $pipe = IPC::PrettyPipe->new;
my $renderer =
IPC::PrettyPipe::Render::Template::Tiny->new( pipe => $pipe );
my $executor =
IPC::PrettyPipe::Execute::IPC::Run->new( pipe => $pipe );
$pipe->renderer( $renderer );
$pipe->executor( $executor );
Here the IPC::PrettyPipe object is passed to the backend constructors; this is necessary if native Moo method delegation is used (more on that later).
To cut down on the boilerplate, the interface should also allow specification of just the class:
my $pipe = IPC::PrettyPipe->new;
$pipe->renderer( 'Template::Tiny' );
$pipe->executor( 'IPC::Run' );
It's also a requirement that the backends be specifiable during object construction:
my $pipe = IPC::PrettyPipe->new( renderer => 'Template::Tiny',
executor => 'IPC::Run' );
Finally, the backend object (however it's created) must be directly retrievable so that any unique functionality is exposed:
my $renderer = $pipe->renderer;
The combination of the dual nature of the backend specifications and the requirement for specification during and after object construction added more complexity than I'd initially expected, as will be seen in the following discussion.
Attribute Coercion
Attribute coercion seems the perfect solution to handle the dual nature of the backend specification. The coercion code is run whenever the attribute is set.
use Safe::Isa;
has executor => (
is => 'rw',
coerce => sub {
# create the object if the attribute can't run()
$_[0]->$_can( 'run' )
? $_[0]
: _backend_factory( Execute => $_[0] ) },
handles => [ 'run' ],
lazy => 1,
default => sub { 'IPC::Run' }
);
where _backend_factory
is a generic factory for backends (see the Appendix).
Unfortunately, this code won't work. Backend objects constructed by the coercion routine know nothing about the pipeline object:
- delegated methods aren't passed any extra parameters to the delegated object;
- The coercion coderef isn't passed a handle to the object, so can't pass it to the backend constructor.
I could explicitly create delegating methods to work around the first behavior:
sub run { $_[0]->executor->run( $_[0] ) }
but that destroys the elegance of the built in method delegation.
Additionally, it doesn't help with the situation where a backend object needs access to the pipeline object during its construction.
Triggers are a wonderful thing
The mechanism which constructs the backend object must
- get a reference to the IPC::Pipeline object; and
- get called whenever the attribute is changed
That would seem to be a trigger. The trigger would decide whether an
object needs to be constructed, and if so, would update the attribute
with the object. _backend_factory
is now a method which passes
along the IPC::PrettyPipe
object to the backend object's
constructor:
has executor => (
is => 'rw',
trigger => sub {
my ( $self, $val ) = @_;
$self->executor( $self->_backend_factory( Execute => $val ) )
unless $val->$_can( 'run' );
},
handles => [ 'run' ],
lazy => 1,
default => sub { $_[0]->_backend_factory( Execute => 'IPC::Run' ) },
);
By their nature triggers are called whenever the attribute is set, so if we set the attribute in a trigger we descend into infinite recursion hell as the accessor calls the trigger which calls the accessor which calls the trigger...
(Technically this particular example won't, as _backend_factory
will
return an object which passes the $_can('run')
test so that the next
call to the trigger will return without setting the attribute. But it
is not necessarily a design pattern I'd generalize.)
There's still some code duplication: with current Moo (1.000007)
setting an attribute value via its default subroutine does not invoke
a trigger, so the default for executor
has to call
_backend_factory
. It'd be nicer if I could specify
default => sub { 'IPC::Run' }
Note that the backend object is now created when the executor
attribute is read or written. I'd prefer it to be only when
read, but this approach doesn't allow that.
A Trigger with a proxied attribute
Continuing in this vein, to avoid possible run-away recursion one
could make the executor
attribute a proxy for a hidden attribute
which does the work:
has executor => (
is => 'rw',
trigger => sub {
my ( $self, $backend ) = @_;
my $executor = $backend->$_can( 'run' )
? $backend
: $self->_backend_factory( Execute => $backend );
$self->_set__executor( $executor );
},
);
has _executor => (
is => 'rwp',
init_arg => undef,
handles => [ 'run' ],
lazy => 1,
default => sub { $_[0]->_backend_factory( Execute => 'IPC::Run' ) },
);
All internal code would use _executor
. This has the same
pseudo-lazy quality of object construction as the previous example,
and still has the duplicated calls to _backend_factory
. This is
safe from infinite recursion, but unfortunately it's now impossible to
guarantee retrieval of the actual backend object.
If the executor
attribute is never set, the _executor
attribute
will get a default value, but that isn't passed back to the executor
attribute. And if executor
was passed a module name, its accessor
returns that name, not the actual backend object generated by the
trigger. The key here is that the value in _executor
is always
correct. We just need to retrieve it.
One way of doing this is to write a custom accessor routine which
knows to read from the _executor
attribute and write to the executor
attribute:
has executor => (
is => 'rw',
reader => '_get_executor',
writer => '_set_executor',
trigger => sub {
my ( $self, $backend ) = @_;
my $executor = $backend->$_can( 'run' )
? $backend
: $self->_backend_factory( Execute => $backend );
$self->_set__executor( $executor );
},
default => sub { $_[0]->_backend_factory( Execute => 'IPC::Run' ) },
);
# doesn't work in Moo 1.000007
sub executor {
my $self = shift;
return @_ ? $self->_set_executor( @_ )
: $self->_executor;
}
has _executor => (
is => 'rwp',
init_arg => undef,
handles => [ 'run' ],
lazy => 1,
default => sub { $_[0]->_get_executor },
);
Unfortunately the above code doesn't actually work under current
(1.000007) Moo, as the custom executor
accessor will be overwritten
by the standard one provided by Moo (because it is created at run time
while the custom one is created at compile time). This is being
addressed by the Moo developers.
A workaround is possible: explicitly specify the phases during which the attribute and custom accessor are created so that the custom one is not overridden:
BEGIN {
has executor ...
}
INIT {
no warnings 'redefine';
sub executor{ ... }
}
This works, but it's kludgy.
There's a also possible future problem. If (ever) Moo invokes
triggers upon default value creation, there's a possibility of
internal crosstalk when the run
method is called if executor
is
not explicitly set. In that case the call stack looks like this:
proxy run()
_executor.default
executor.default
executor.trigger
_set__executor
_executor
's default value constructor eventually calls its
own setter. This may not be a problem. Or it might.
Two attributes are better than one, unless there are three
The above approach shows that using two attributes provides the separation required to circumvent possible infinite recursion. However, exposing either of them directly to the user require excess machinery to control their behavior.
In the next iteration both attributes are private. One attribute holds the user's backend specification, the other holds the actual backend object. The object is created on-the-fly from the specification. When the specification changes, it triggers the clearer on the attribute holding the object, so that the next time that attribute is accessed the backend object it is recreated from the specification.
To create the semblence of a single attribute the private
specification attribute is initialized via the executor
keyword
to the constructor, and a custom executor()
"accessor" was written to
dispatch to the appropriate attribute for reading or writing.
has _executor_arg => (
is => 'rw',
init_arg => 'executor',
default => sub { 'IPC::Run' },
trigger => sub { $_[0]->_clear_executor },
);
has _executor => (
is => 'rw',
handles => ['run'],
lazy => 1,
clearer => 1,
default => sub {
my $backend = $_[0]->_executor_arg;
return $backend->$_can( 'run' )
? $backend
: $_[0]->_backend_factory( Execute => $backend );
},
);
sub executor {
my $self = shift;
$self->_executor_arg( @_ )
if @_;
return $self->_executor;
}
No duplicated code, no worries about tricky code paths. Not as concise as I'd have liked it. The backend object is still built upon reading or writing.
Attribute Accessor Modification
One final approach is based upon the realization that the attribute doesn't need to be coerced immediately that it is set. This can be done using a single attribute by wrapping its accessor to perform coercion at access time.
has executor => (
is => 'rw',
handles => ['run'],
lazy => 1,
default => sub { $_[0]->_backend_factory( Execute => 'IPC::Run' ) },
);
around 'executor' => sub {
my ( $orig, $self ) = ( shift, shift );
my $attr = $orig->( $self, @_ );
unless ( $attr->$_can( 'run' ) ) {
$attr = $orig->( $self,
$self->_backend_factory( Execute => $attr ) );
}
return $attr;
};
The attribute default sub still has to call _backend_factory
because
the accessor is not invoked if the delegated run
routine is called
and no attribute value was specified.
The drawback with this approach that you get hit with the overhead of the coercion check with each retrieval of the attribute; with the other approaches it was only when the attribute is set.
Conclusion
In the end I settled upon using the approach with two private attributes and the custom accessor binding them. I like the simplicity of the trigger/single attribute approach, but I'm worried about code maintenance and the subtlety of ensuring there's no infinite recursion.
The implementations were more complex than I originally thought necessary for what seems to me to be fairly simple requirements.
There are a few aspects of the Moo object model that drove things in that direction.
If the coercion routine had passed along the object, I think most of the complexity would have disappeared. While there's no guarantee that the object will be in a useable state if the coercion routine is invoked during object construction, the reference to the object is still useful as an opaque handle.
If one could disable triggering within a trigger handler (much as in a signal handler), that would have greatly weakened the argument for attribute proxying.
attribute setting via the default method doesn't set off the trigger handler. There may be a reason behind this behavior (or it might just have been overlooked).
Moo doesn't seem to use attribute accessors internally (for speed I imagine), so modified or overridden ones don't always get called when the attribute is accessed.
If run-time flexibility weren't important, I think that role composition wins hands down.
I'm intrigued by the possibility of abstracting the code so that one could use either the rather more elegant role composition or method delegation.
Appendix
For completeness, here's the final implementation of _backend_factory
:
use Module::Load qw[ load ];
use Module::Path qw[ module_path ];
use Module::Runtime qw[ compose_module_name ];
sub _backend_factory {
my ( $self, $type, $req ) = ( shift, shift, shift );
my $module = compose_module_name( "IPC::PrettyPipe::$type", $req );
croak( "unknown $type ($req => $module)\n" )
unless defined module_path( $module );
load $module;
return $module->new( pipe => $self, @_ );
}
> with current Moo (1.000007) setting an attribute value via its default subroutine does not invoke a trigger
Moose has never done that either, so that would be a major compatibility problem if we changed it.
> If the coercion routine had passed along the object
I do wish there was an elegant way to coerce a value with the object available, but again, Moose doesn't do that either.
Pending a better idea, I've been using something similar to your 'two attributes are better than one' approach.