To extend any framework you need to be able to hook into that framework’s initialization process.
That Damned Context
The Robotlegs 1 context class was, to be frank, rubbish – it set defaults, configured dependencies and controlled initialization. Worse, to hook into the initialization process or change the defaults, users had to extend the class itself resulting in the proliferation of funny looking words like SignalDrivenCovariantlyMediatedModularShellContext.
How Could You Let That Happen?
Extending the context was a silly idea – and I knew it at the time. It made me sad, deep down inside, but I knew that if I waited for a better solution to emerge the framework might never ship.
Also, my goals for Robotlegs 1 were:
1. Make it small
2. Make it fast
3. Release it
A ton of compromises were made to keep the number of classes down to an absolute minimum. That’s not how I usually write code. Besides…
Small Only Goes So Far
I always start by doing the simplest thing possible – it’s a good way to feel out a problem space. “What am I trying to do?”. The “how” doesn’t matter yet, but becomes important much later when I ask “How would I like to do this?”.
When writing actual code, however, I’m constantly faced with this annoying question: “Should I break this out?”. When doing the simplest thing possible I often decide against extraction: I’ve got a specific task to complete (like getting a test to pass) and I can refactor later if the idea proves itself to be worthwhile. Also, I know that extraction inevitably leads to a new set of design decisions. And I’ll have to start with a test – for something whose purpose I am only just discovering. And it ups the class count.
Keeping class count down for it’s own sake is almost always a bad idea. However, I don’t think Robotlegs 1 would have experienced the same adoption if it had had hundreds of classes instead of the quite digestible 15 presently in its belly. OK, 24 total if you add the interfaces.
When browsing open source libraries I get happy feelings from projects with a limited number of source files – it gives me a sense that if I really wanted to I could read through the entire codebase and understand it.
But there’s a point where that simplicity inverts. Profit becomes debt. Without good abstractions in place every participant in the system ends up doing too much. At that point the question becomes: “Do I need to introduce a new layer of abstraction?”.
Now You Have Two Problems
The introduction of a system wide abstraction layer fueled by a mid feature code extraction will do one of two things: it will strengthen the system or it will weaken it. A bad abstraction might solve the immediate design issues (like duplication) just fine but end up adding complexity and reducing quality overall.
So anyway, I want Robotlegs to be extensible in a sensible way. Users should be able to install custom extensions easily. Those extensions need a way to hook into the context initialization process:
Some extensions may need to be configured before others. Some may need to pause the initialization process and wait for a resource to become available. Some might directly depend on others and may need to wait for those extensions to be fully configured before self initializing. Some may only come to life after the entire context has been initialized.
It’s clear that the initialization process needs to be asynchronous. But without a good abstraction in place for dealing with asynchronous processing the context will be burdened with co-ordinating a complex series of events. Event soup is not delicious.
Events, Signals, Tokens, Tasks, Promises, Futures, Callbacks
There are a number of patterns for dealing with asynchronous processes. They have wildly different properties.
Some are low level, like the callback pattern. Some are high level, like the promise pattern. Some are guaranteed to be asynchronous, while others have the potential to be synchronous. Some are only good for observing. Some can be terminated, while others can be suspended and resumed. Some can be composed and chained. Some necessitate the instantiation of throw-away objects and put stress on the garbage collector. Some put stress on the stack.
Take Your Pick
I wanted something fairly low level that enabled asynchronous processing without adding unnecessary overhead in the cases where things could be synchronous (or skipped entirely).
I tried a number of things. For the most part: floppy, verbose and annoying implementations that moved the core problems around in less-than-useful ways.
Slowly it dawned on me. In this case all I wanted was an asynchronous message dispatcher. With a well defined set of callback conventions I could write something resembling an Event Dispatcher but without the overhead of creating throw-away objects (events) and with the ability to suspend and resume the dispatch when desired:
Initialization, Extension, Please Continue
Very well. Suppose that context initialization comprises 3 steps: preInitialize, selfInitialize and postInitialize. When told to initialize the context moves through those steps in sequence. Anyone can listen in and halt the process as required at any point. What we end up with is in essence just a simple state machine where the observers have the potential to be guards.
Well, that’s where the re-write started anyway.
p.s. I dropped a bunch of stuff from the version2 branch. I’ll be going back to pull out any functionality that might have been lost shortly. If you come across anything that’s missing feel free to adapt it to the new codebase and send a pull request. Or drop an issue on the tracker. For quick reference, here’s the old version2 branch: version2-old