Rolling Your Own Composition
Beyond Method Modifiers
Moose offers a few different kinds of method modifier, or "advice." They are before, after, and around. (There's also "augment" and "override," which are only sorta kinda method modifiers.) Advice lets you change the behavior of classes in very useful ways, and you can put advice in roles. The problem is that it is often more limited than you might think.
For example, subclasses of classes with advice can entirely clobber the advice applied to the superclasses' methods. Methods that collect output from multiple pieces of advice are a bit painful to write.
augment solves that fairly well, but can't be used in roles, greatly limiting that behavior's usefulness. I wanted to write methods in which all the roles in a class could contribute behavior -- but subclasses-clobber-advice, combined with no-augment-in-roles made this a really annoying problem.
Sure, it could all be done with
around modifiers, or by doing a lot of crawling of the meta objects, but that would be really annoying to implement over and over, and would be pretty confusing to skim. I wanted to write a library that made the pattern simple to use, not to just solve my current problem once.
This calls for specific example.
A very simple example is tagging. We want our objects to have a
tags method that returns a list of tags or keywords that apply to an object, and any roll or subclass can add more without affecting the tags added by anyone else.
With this class, we'd expect this:
We can't just use
sub in classes and
around in the roles, because the subroutine in Gift::Christmas would blow away the advice applied in Gift. We can't just use around everywhere, because if we tried to apply Role::Gratis to a class without a
tags method, we'd get an error. We can't even do a silly "provide an empty
tags method" trick, because Role::GiftWrapped would have to do it, too, and they would conflict. (If none of this is clear, consider reading about roles, advice, and BUILD in Moose.)
Instead, we can use MooseX::ComposedBehavior to make that
add_tags thing above actually do just what we want. First, we write a TagProvider library:
Then, we add
use TagProvider to each of the classes. That's it!
add_tags will exist, adding more things to the list of stuff to return. When the
tags method is called (notice it named by the
method_name parameter, above) it calls all the
add_tags blocks and uses the
compositor to combine them. Here, the compositor just makes a big set of all the tags returned by every unit of composition -- roles and classes alike. There's no need to worry about overriding or clobbering anything.
We've been working one some code that generates and responds to events. When an object does the HandlesEvents role, it will have a registry of event names, and each name will have a bunch of handlers, also registered by name. So, we might have a registry like this:
When an object with that registry get a
got_signal event, it calls each of the event handlers.
A lot of these handlers are going to be really common. For example, every object should be able to respond to a heartbeat event, even if only to respond by making note of it. We want to be able to put implicit event handlers on roles, so that when we compose together actual objects, they start off with all the event handlers that they should. For example, we might write these two packages:
Then, say we produce this class:
It will have the following set of handlers implicitly:
If there's ever a conflicting set of entries, we should throw an exception. This turns out to be easy with MooseX::ComposedBehavior, too. We just write a ImplicitEventHandlers package and use it in all the packages above. This is what goes in it:
When we want to know an object's composed event handlers, we just call its
composed_implicit_event_handlers method. We can even make that method a builder for a lazy attribute, if we want to avoid recalculating.
Wacky New Composition is a Strategy of Last Resort
Building new ways of combining methods across compositional structures is neat. It solves a number of problems that might be hard to solve without it, and it's a very shiny little tool. It's also weird. Code that does this becomes harder to skim, and harder to explain, and more tied to a wonky library implemented in a clever but unfortunate way.
The existing tools for re-use provided by Moose are very powerful, and can address a huge array of problems. MooseX::ComposedBehavior is useful only when you're quite sure that you've exhausted simple-enough solutions wiht Moose.
roles, advice, and BUILD in Moose - an introductory blog post about why composed behavior can be confusing
composing your own behavior across Moose class structures - a more detailed follow-up about MooseX::ComposedBehavior