Over the last few months I’ve knocked up a few specialised ‘maps’ for working with robotlegs. I like the ethos behind these kinds of utilities – abstracting the meta-logic (the nature of the problem being solved) away from the detail-logic (the specific solution).
I’d planned to blog each one in turn, but the time runs away so quickly doesn’t it?
And so – all in one fell swoop – I offer you 3 variations on the CommandMap, a SignalMap and a race-condition beating EventMap. All on github and created through Read-Me-Driven-Test-Driven-Development, so you should find the ReadMe in each case sufficient to get going, but do check out the diagrams in this post as well:
- GuardedCommandMap – for separating conditions from actions
- OptionCommandMap – for implementing user options
- CompoundCommandMap – for triggering a command in response to a combination of events
- SignalMediator (containing SignalMap) – for automatic cleanup of signal handlers
- RelaxedEventMap – for beating race conditions
1. GuardedCommandMap – separating conditional logic from action
Sometimes you want to map behaviour to an event, but only if certain other conditions are met.
It could be that the condition is relevant to some other property of the event – say the specific key being pressed in a KeyboardEvent – or it could be something else, for example whether the user already has local account details in a SOL.
Usually we wind up implementing this kind of logic using if() statements and early bails in the execution of the Command.
The GuardedCommandMap abstracts the conditions from the actions.
As well as mapping a Command, you also map one or more Guards. The Command is only executed if all the Guards agree to it.
This has the advantage of allowing you to map a Command as oneShot, but know that it won’t be executed and unmapped unless all the Guards are passed. It also avoids duplication of condition-checking logic – for example if you had multiple commands that you only want to kick in after the ‘training’ phase of a game. You can map the same Guard (or Guards) to more than one Command.
What’s a Guard?
A Guard is very similar to a Command. It has only one public method:
The Guard Classes are instantiated in the same way as Command classes – so they can have injections in the same way as the Command, and can receive the Event class that triggered the CommandMap, just as the Command eventually will.
approve() method returns true or false. If all the
approve() methods return true then the Command will be instantiated and will run. If any
approve() method returns false then the process is aborted.
2. OptionCommandMap – a generic ‘user choice’ solution
This is a commandMap which allows you to map against a common event type, and which automatically cleans up (removing all mappings) once one of the options has been selected and actioned.
Why would you need this?
OptionCommandMap is designed to streamline the following use case:
- A user is offered multiple options
- Based on which option they select, a different command (or combination of commands) should be run
- Once the appropriate command is run, the previous option-command pairings are no longer required
Note: The use case happens repeatedly at runtime, with variations in the options and the commands, but a persistence of the overall pattern – the user is making a choice, and based on that choice a command should be executed. (Note that ‘user’ could be substituted with an external service or client). For example:
- Your dope-wars clone game pops up a user decision dialog “You’ve run out of cash! What would you like to do?” with options “Beg”, “Borrow” or “Steal”
- Depending on which option the user chooses, a different command runs to update models within the game to reflect the user’s choice
- Once the user has made their decision, the other options are no longer viable
Why not just use the ordinary CommandMap?
You could achieve the above by doing the following mappings:
commandMap.mapEvent(OutOfCashEvent.SELECTED_BEG, TryBeggingCommand, OutOfCashEvent); commandMap.mapEvent(OutOfCashEvent.SELECTED_BORROW, TryBorrowingCommand, OutOfCashEvent); commandMap.mapEvent(OutOfCashEvent.SELECTED_STEAL, TryStealingCommand, OutOfCashEvent);
but that would result in a lot of bespoke events for each possible option offered to the player. Generally I’m in favour of strong typed events, but in the case of a game where there might be hundreds of randomly generated options, we can do better.
If we decide to use a common OptionEvent and make mappings at runtime such as:
commandMap.mapEvent(OptionEvent.OPTION_A, TryBeggingCommand, OptionEvent); commandMap.mapEvent(OptionEvent.OPTION_B, TryBorrowingCommand, OptionEvent); commandMap.mapEvent(OptionEvent.OPTION_C, TryStealingCommand, OptionEvent);
then we need to also keep unmapping these events, as once an option has been selected, the rest of the mappings (which may be many, there could be several options) need to be unmapped, so that when we next map options – perhaps for ‘Hide stash’ or ‘Flush stash’ options in a raid – we don’t also kick off the Command for the relevant Beg / Borrow / Steal options.
The OptionCommandMap is intended to streamline this process and make the cleanup automatic, as well as simplifying the setup.
3. CompoundCommandMap – execute based on a combination of events
Sometimes we only want our application to respond after a number of processes have completed, but we can’t be sure that they’ll happen in any particular order.
Maybe your startup cycle involves several different asynchronous process. You only want your application to move into its ‘active’ state once all of those processes are complete.
Maybe your command needs the payload from more than one of those events – and it’s a bit of a PIA to have to store event values in an intermediate state solely for this purpose.
The CompoundCommandMap encapsulates the ‘checking that everything has happened and hanging on to any values’ logic, allowing you to map a Command to a combination of Events – in a specific order or in any order. All the events are available to be injected into your Command when it is triggered.
It even supports named injections so that you can inject multiple instances of the same Event class.
// params: Command Class, isOneShot, requiredInOrder compoundCommandMap.mapToEvents(SomeAwesomeCommand, true, true) .addRequiredEvent(SomeEvent.SOMETHING_HAPPENED, SomeEvent) .addRequiredEvent(SomeOtherEvent.SOMETHING_ELSE_HAPPENED, SomeOtherEvent, 'somethingElseHappened') .addRequiredEvent(SomeOtherEvent.STUFF_HAPPENED, SomeOtherEvent, 'stuffHappened');
4. SignalMap (and SignalMediator)
The trouble with listening for events and signals is that it’s damn easy to forget to unlisten!
The SignalMap was originally intended to replicate the robotlegs EventMap, allowing the Mediator to do auto-magic clean-up of your signals when the view leaves the stage. But the SignalMap has no dependencies, so you can exploit it anywhere that you want to be able to clean up a number of Signal listeners in one go.
5. RelaxedEventMap – what race conditions?
Mediator: “Ok, I’m ready, my view’s on the stage, can I get updates on any data changes?”
Mediator: “Uh… come on then, it’s been like 5 seconds, and nothing’s happening! I need some data to pass to my view.”
EventMap: “Really? I’m sure I passed some data on a while back. You didn’t hear that?”
Mediator: “No! Are you sure? I’ve been listening *really* carefully.”
EventMap: “Yup. I’m sure. It was right before you said you were ready.”
Mediator: “Before? Wait! No! But I wasn’t ready then – I wasn’t even around… can you send it again?”
EventMap: “Sorry dude. You’ll have to wait for an update – I didn’t keep any notes, I’ve no idea what the message said.”
The RelaxedEventMap is an EventMap with built in time-traveling capabilities. At the time of registering a listener, it looks back in time for the last instance of the relevant event, and if it finds one then it relays it to the listener immediately.
Race-conditions be gone!