Thursday, May 24, 2012

Dependency Injection with Moose

I'm close to finishing the initial release of Hypatia. The internal part of the API has changed quite a bit, thanks to suggestions by Cory 'G' Watson. The biggest change by far is the ability to call what back-end you want and to have that load automatically, eg

What makes this a bit more difficult than simply loading the module given by the back_end attribute and moving merrily upon our way is that these modules have lots of methods (eg chart) that should be used by the base object. However, this is not insurmountable, and Moose provides two ways to get around this (TIMTOWTDI, after all): loading dependencies as roles or as modules.
For the sake of illustration, let's look at something simpler than Hypatia (but still as geeky): writing an object system for characters in a fantasy RPG setting. For simplicity's sake, let's also assume that we have the standard classes of Fighter, Rogue, and Mage, and races of Human, Elf, Dwarf, and Halfling.

Etc, etc. As of right now, class and race are just strings. However, there may be a lot of things that depend on race and class, such as stats (eg halflings are more dexterous than dwarves but are weaker), skills/abilities (elves are better with magic but are more frail), and both classes and races can have equipment restrictions (halflings can't wield polearms and mages can't wear plate mail). Now yes, it's possible to go through a bunch of if-then statements to suss the logic out in one place, but what if you add other classes or races? Down this way lies spaghetti coding

So, let's take a look at getting around this via roles and via modules. In either case, it's BUILD to the rescue!

Roles (consumed into your main class)

All that we need to know is which roles go with which combinations of class and race. This has to be hard coded somewhere, but this hard-coding will be the single, unambiguous, authoritative representation of these dependencies within our system.

So, if, for example, we've chosen a halfling rogue and we've stored the roles that we need to load in the array @roles (containing, say, Stat::Bonuses::Halfling, Allowable::Equipment::Small, Skills::Rogue, etc), then our BEGIN would look something like this:

And that's prettymuch it. If all goes well, the roles should be loaded. The only thing you need to watch for is to avoid duplicating method or attribute names within these roles, but that should be easy enough if you design them carefully.

Modules (loaded into attributes)

The idea here is somewhat similar, but a bit different in that we're loading modules based upon our choices into attributes of our main class. If you want to call upon attributes and methods of each of these class/race related module within your main Character module, then you'll have to use handles.

Of course, it might be tempting to immediately pull down every method and attribute from each class via handles=>qr/.*/, but this is actually a bad idea since this also includes the Moose-specific magical BUILD and BUILDARGS methods. So, we need to be a bit more specific with our regex.

Also, to avoid confusion, it's a good idea to name the attributes differently from the corresponding module. So, let's assume that we've stored this information in the hash %modules (which might, for example, contain a key-value pair of "skills"=>"Skills::Rogue"). We'll also make use of Module::Load's load function:

Unfortunately, I've skipped over a couple of potentially important details....what if you want to pass arguments to the constructor of $module that were initially passed to the main class (this is definitely the case in Hypatia)? Then you'll have to sift through that information beforehand, either before the while loop above or separately in BUILDARGS. Also, what if you don't always want $attr to be read-only? Well, you'll have to suss out that information (including it in %modules, perhaps) and load that dynamically.

Summary

So, dependency injection in Moose can be done at run-time by either dynamically loading classes or by tying modules to attributes. The first approach seems simpler and is less likely to shatter the encapsulation-fourth-wall. However, it leaves all of the attributes and methods of all of the dynamically-loaded roles sitting in your primary class. The second approach does a better job of bucketing these attributes and methods, but can be much more difficult to set up if you have to pass arguments along (believe me...I've learned this the long and hard way).

No comments:

Post a Comment