Recently, I've been experimenting with various microarchitectural frameworks. I decided that it might be useful to others to share my thoughts as I work through this process as an absolute framework beginner. Last time, I tackled Maté. This time, I've been playing with Robotlegs.
I'd like to begin with a big "thank you" to Joel Hooks, whom I met when he was presenting Robotlegs training at 360|Flex. I don't think I was ever going to completely understand what he was trying to say there unless I got Robotlegs out and started playing with it myself, and Joel has been tremendously helpful and supportive through the process, from fixing issues with the Robotlegs site that were slowing me down to answering my random questions.
Getting Started with Robotlegs
The first thing you need to do is stop by Robotlegs.org and download the latest version. What you'll get is a zip file that includes the source code for Robotlegs, its api documentation, and two swcs, one in the bin folder and one in the libs folder.
The one in the bin folder is the actual Robotlegs framework, but the one in the libs folder is equally important. That swc is SwiftSuspenders, which supplies the default injector used by Robotlegs. I'll get into what I mean by default Injector later. For now, the main thing you need to know to use Robotlegs in Flex is that you need to put both these swcs in the libs folder of your Flex project.
Even though the source code for RL is provided, it's important to use the swc, because part of what it does is makes sure that the Metadata used by the framework gets compiled into your code instead of discarded, which is what the compiler would do by default.
Context in Robotlegs
For me, the most important concept to understand in RL was what the Context is and what it does. The Context Class is the "glue" that holds all of the other elements of RL together. It instantiates an EventDispatcher that is used by all of the Classes used in the default MVCS implementation.
What's a default MVCS Implementation?
I'm glad you asked. Robotlegs defines almost everything in terms of Interfaces, which means the actual Classes used in Robotlegs can be swapped out for other classes that implement the same Interface. So, in theory, you could create an entirely different implementation of Robotlegs if you wanted.
Which brings me back to SwiftSuspenders as the default Injector for Robotlegs. In fact, it's really the default IInjector, as Robotlegs does not define an Injector Class at all. You could use other IoC frameworks, such as SmartyPants (but you might need to write some adapter code to make it work).
SwiftSuspenders was written specifically to support Robotlegs, and the Context class will create a SwiftSuspenders Injector for you when you use that Class as your IContext. This Injector is stored in a protected property called, not surprisingly, injector, that you can use when you extend Context.
What does the Injector do?
If you recall, I said it was important to use the swcs to make sure that the Flex/Flash Builder compiler will keep the metadata needed by Robotlegs and SwiftSuspenders. When you add the [Inject] Metadata tag before a declaration and Robotlegs interacts with that Class, the Injector will check its "little black book" for an object of whatever type is needed by the declaration, and then that object will be injected, usually as a variable value. If an object of that type does not exist, it will make one.
Even before you do anything with the Injector the Context has made for you, the Context will add a few items to its "black book" for you, such as the EventDispatcher it made and a reference to the Injector itself. It's important to realize these mappings are there, in part so that you don't inadvertently interfere with them. But if you know they are there, you can also use them, in the way that the default MVCS Classes, such as Actor and Mediator, do.
There are two ways that Robotlegs can interact with a Class instance in order to inject values.
First, it can create that instance in a variety of ways. For example, if you write injector.mapSingleton(YourClass), then if the Injector needs to inject a "YourClass" into a property of another class, it will create a YourClass. If YourClass has any [Inject] tags, then those will be filled right then, when the new YourClass is created.
The second way is that the constructor for Context takes an argument of a DisplayObjectContainer, which defines what the Context is a context for. The Context adds an event listener for ADDED_TO_STAGE events, and so when new View components get added, the Context will check its maps to see whether it needs to do anything about them. You might be saying "But ADDED_TO_STAGE doesn't bubble. How can that work?" Turns out that you can set the useCapture flag to true and listen for this event "on the way in," as it were.
The main thing to remember, though, is that the Context will be notified when any child, grandchild, etc. of the DisplayObjectContainer it is associated with gets added to the stage. At that point, it can either inject into the newly created object, or create a Mediator and inject the new instance into that.
This gives you a rough idea of how data moves from one place to another in Robotlegs, but there's more to how an Application works than that.
Making things happen in Robotlegs
The Context sets up a CommandMap, which essentially says "when the Context's EventDispatcher dispatches event X, create an instance of Command Y and run its execute() method."
What's a Command?
A Command is essentially a short-lived Object whose sole purpose in its short life is to execute a set of instructions. The command will receive any objects/information it needs to carry out those instructions the same way that all objects Robotlegs instantiates do–they are injected by the Injector. This includes a reference to the Context's eventDispatcher and injector properties.
Using Robotlegs made me realize that I had built up a great deal of prejudice that a Class is something. As a side effect, it may also do things, but its primary purpose is "what to be," not "what to do." Once I put this prejudice aside, I fell completely in love with the Command pattern and the CommandMap. It codifies what the intent is of each of the events you choose to dispatch, allowing for extremely expressive code.
I normally comment the heck out of my code, but I put almost no comments into the parts of the code that were new to adapt my example for RobotLegs. Comments weren't that necessary, because the names of my Commands (GetTaxonomyRoot, AttachChildrenToModel, etc.) made it really obvious what was supposed to happen when a given event fired.
For me, the most difficult part was figuring out what the right entry point was (which event kicks off the process and which Command should go first). Fortunately, I still had my files from the 360|Flex training, so I just used the same event Joel used in his Context (ContextEvent.STARTUP), and with a very little experimentation I figured out which of my Commands made sense to use first.
What can a Command do?
By default, Commands have access to the Context's commandMap, the DisplayObjectContainer the Context is associated with, the EventDispatcher, the injector, and another property of the Context I haven't talked about yet, the mediatorMap. This means that they can add other Commands to the CommandMap, add listeners to the DisplayObjectContainer, dispatch events to the Framework, and map more classes to the injector. In addition, they have access to whatever other information you choose to give them by usin [Inject] metadata. This expands your possibilities even further.
One type of data that seems to be difficult, if not impossible, to inject into a Command is any object that is added to the display list that the Context applies to. We already know that we can inject data into a View component when it is first added to the stage, but how do we change what it is displaying when the Application state changes, or notify the Application when a user interacts with that View?
Communicating with Views using Mediators
Robotlegs has something it calls a Mediator, but I think it's more akin to a Supervising Presenter. In short, for every View Class that you want to hook into Robotlegs, the recommendation is that you create a Mediator that has several responsibilities:
- Listening for events from the Context's EventDispatcher that affect the View
- Updating the View's state/data in response to those events
- Listening to the View for changes in its state/data by the User
- Dispatching information about those changes using the Context's EventDispatcher
This allows your Views to stay in sync with the Application and reduces the need for data binding.
You can set up a View to be mediated by using the Context's mediatorMap. When a View in the mediatorMap is added to the stage, Robotlegs will automatically create a Mediator for it of the Class you specify. Instead of injecting data into the View, Robotlegs injects a reference of the View that was just added to the stage into the Mediator it just created to mediate that View. Since the Mediator has a direct reference to the View, it can use its public API and listen for events it generates. You usually set all this up in one of Mediator's methods, such as onRegister(), that are called once all of the injections have happened, including the reference to the View.
Like the Command class, the Mediator class is injected with references from the Context, including the EventDispatcher and the mediatorMap. This allows the Mediator to set up other Views for mediation and to listen for and dispatch events that can be used in any instance that is hooked into the Context.
Usually Views are not working on an entire data set, but instead are editing just a part, so it becomes important for the larger model to be notified when something changes. We've already covered how a data class can be injected with data when it is created by Robotlegs, but how do we change data–and how do we let the data communicate changes into itself, such as new information arriving from the server?
Communicating with Data Classes
By now, you're probably starting to guess that the team at Robotlegs has provided you with a Class that's already set up to automatically be injected with the dependencies it needs to interact with everything else within the Context. The default implementation of Robotlegs suggests that you extend Actor for Model and Service objects. Actor is simply another class that has hooks to receive the Context's EventDispatcher, etc.
I wound up not using Actor, since I already had data Classes that were set up to receive an EventDispatcher as a constructor argument from my experiments with Maté. Instead, I used constructor injection and allowed the Context's injector to supply a reference to the default EventDispatcher. It took me a while to find any documentation on how to enable constructor injection, but it turned out to be as simple as using the [Inject] tag before my class definition.
I'm not entirely happy with having to alter the definition of my core classes to work with a particular Framework, but the alternative would have been to wrap my data classes in an Actor, which would have been a pain.
I think there's a lot to love about Robotlegs. Once I figured out what was going on with Robotlegs, it took me very little time to retrofit my Maté code to use Robotlegs instead–about an hour and a half. I like that it doesn't depend on binding, and almost actively discourages it. I think this speaks well to the scalability of the solution. I'm also intrigued that there's an extension that supports As3 Signals, as the more I learn about good practice the less I like Flash Events.
I also really like the implementation of the Command object and how it let me get right into writing code that was more or less self documenting.
On the down side, you have to put the [Inject] tag in any class that needs to receive information from Robotlegs. This can be a real pain for a Class that it doesn't make any sense to alter. For instance, I have a Tree component in one of my Views that needs to be injected with a custom DataDescriptor. I couldn't bring myself around to a point of view where it made any sense to subclass Tree and give its DataDescriptor property a metadata tag to allow for injection, so I moved the responsibility from the Injector to the Mediator, which felt like a hack.
I think that it's "neato cool" to be able to use metadata tags and describeType, but from a practicality standpoint, it seems to me that you should be able to say from outside the class that "I want to inject into property X." It's just not always possible or desirable to change a class just to allow for injection. In the case of Views that you're setting up for injection, the Context already has a reference to the Class it's mapping to, I don't believe it creates any tighter coupling to refer to a specific property that you want injected into.
It might be possible to combine XML injector configuration with the default Metadata configuration to overcome this when it doesn't make sense to alter the Class definition to make injection work, but I'd still like to see a little more clarity and specificity to how this works.
That being said, I think it's an amazing start for a Framework that's almost exactly a year old. Have you used Robotlegs? What do you think?