A design pattern that has cropped up a lot in my Flash / AS3 endeavours is what I call the "Worker and Consumers" pattern.
1. Context
Multiple asynchronous calls are made. The first of these calls takes time to complete, but once complete, the result of the call can be memoized.
1.1. Example
When loading image resources, each image resource can be identified by a URL. Once the image resource has been retrieved and cached, further requests to load from the same URL can be served from the cache.
2. Problem
Managing the calls and recipients of the image data so that a minimum of calls are made.
3. Solution
First, each call is identified by a key. Calls with the same key produce the same results.
Second, we split the processing into two steps - the work step and the consume step. The former does the hard work, the second acts on the memoized result.
Third, we split the callers for each key into two sets: A set containing a single worker, and a set containing the rest, the consumers.
We define the following interface:
interface Caller {
/**
* Returns the memoization key for this call
*/
function getKey () : String;
/**
* Performs the time-consuming work.
*
* @param onComplete a function taking a
* single Object parameter, called when the
* work is completed.
*/
function work (onComplete:Function) : void;
/**
* Invoked for all callers, worker and consumers,
* to process the result.
*/
function consume (result:Object) : void;
}
We then maintain two maps. One is a map from keys to the single worker: String -> Caller
. The other is a map from keys to the vectors of consumers: String -> Vector.<Caller>
.
The process is then as follows, in pseudo-AS3 code where some declaration have been left out for brevity:
function call (caller:Caller) : void {
var key:String = caller.getKey ();
// Do we have a result for that key?
if (hasResult (key)) {
// Yes, so consume and return
caller.consume (getResult (key));
return;
}
// Do we have a worker for this key?
if (workers[key] == null) {
// Yes, so add caller as a consumer
if (consumerMap[key] == null) {
consumerMap[key] =
new Vector.<Caller> ();
}
consumerMap[key].push (caller);
return;
} else {
// No, this caller becomes the worker
// for the key.
// Add caller to worker map
workers[key] = caller;
// Start working
caller.work (function (result:Object) : void {
// Save the result
putResult (key, result);
// Pass the result to the worker
caller.consume (result);
// Pass the result to the waiting
// consumers
if (consumers.containsKey (key)) {
var cs:Vector.<Caller> = consumers[key];
for (var i:int = 0; i < cs.length; ++i) {
cs[i].consume (result);
}
}
// Clean up
// Remove worker from worker map
delete workers[key];
// Remove all consumers from consumer map
delete consumers[key];
});
}
}
4. Other Uses
I've also used the same pattern to handle Amazon Glacier retrieval requests. The worker starts the retrieval and stores the archive locally. The consumers then access this local copy.