download this article in .doc format
Introduction
salient.Delegate is a class that enables the implementation of numerous software design patterns that can accelerate design and development and improve the architecture of your JavaScript code by enabling reliable loose coupling of JavaScript code.
Overview
The intention of salient.Delegate is to provide a lightweight, robust and easy to use JavaScript delegate that can be used to implement a multitude of design patterns including Event, Observer, Command, Chain of Responsibility patterns.
These patterns can drastically simplify the architecture of an application by means enabling simple and robust implementations of dependancy inversion and clear seperation of concerns.
Client script applications can be designed in a more modular, or encapsulated, fashion facilitating isolated testing and in turn enabling agile methodologies.
Don't let all of these buzz words frighten you. I am simply describing things that we all do, hopefully, in some degree every day, knowingly or not.
While this article is not intended to be any sort of primer on patterns, we certainly will explore a few that should be quite familiar even if not by their formal name.
Disclaimer:
- This class is not intended to handle DOM events. Of course it can be purposed to that end but there are much more appropriate implementations, notably Dean Edwards' Event implementation.
- The descriptions and interpretations of GOF design patterns presented are just that, interpretations and approximations. Any input regarding these implementations are especially welcome.
Usage
Lets first examine the API:
Listing 1: salient.Delegate API
var Delegate = function(name)
{
};
Delegate.prototype.addHandler = function(fn, context)
{
};
Delegate.prototype.removeHandler = function(handle)
{
};
Delegate.prototype.invoke = function()
{
};
Event/Observer Pattern
Listing 2: Simple event/observer pattern example
var observedType = function()
{
var _foo;
var change = this.propertyChange = new salient.Delegate("observed.propertyChange");
function onChange(name, value)
{
change.invoke(name, value);
}
this.setFoo = function(value)
{
_foo = value;
onChange("foo",_foo);
}
this.getValue = function()
{
return _value;
}
}
var observerType = function()
{
var observed = this.observed = new observedType();
function observedPropertyChanged(name, value)
{
alert("observed property " + name + " new value is " + value);
}
observed.propertyChange.addHandler(observedPropertyChanged, this);
}
var observer = new observerType();
observer.observed.setFoo("bar");
Chain of Responsibility
A traditional Chain Of Responsibility pattern uses a linked list of handlers and the invocation chain is controlled by explicit passing of responsibility within the handlers themselfs. This requires that not only the owner of the COR be aware of all of the handlers, each handler is aware of the next. Additionally, logic must be emplaced to manage a linked list, adding to the complexity of the implementation.
With a bit of imagination we can generalize this pattern and envision it as a simple array of handlers that are invoked in order of appearance, each invocation posessing the power to terminate the invocation chain by signalling cancellation in the return value.
Using salient.delegate, a Chain Of Responsibility is quite easy to implement because that is the design pattern that salient.Delegate's implementation most closely resembles, albeit shaped a bit differently as described above.
With deliberate addition of handlers you may construct your chain of responsibility and pass the subject as an argument in the .invoke() invocation. If, during any handler invocation, the chain should stop, simply return salient.DelegateCancel.
I will leave it to the reader to implement methods to manipulate the handler chain after creation. The handler array is exposed as delegateInstance.handlers.
Listing 3: Simple Chain Of Responsibility pattern example using salient.DelegateCancel
var Coin = function(diameter, weight)
{
this.Weight = weight;
this.Diameter = diameter;
}
var FivePenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 3.25) < 0.02 && Math.abs(coin.Diameter - 18) < 0.1)
{
alert("Captured 5p");
return salient.DelegateCancel;
}
}
}
var TenPenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 6.5) < 0.03 && Math.abs(coin.Diameter - 24.5) < 0.15)
{
alert("Captured 10p");
return salient.DelegateCancel;
}
}
}
var TwentyPenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 5) < 0.01 & Math.abs(coin.Diameter - 21.4) < 0.1)
{
alert("Captured 10p");
return salient.DelegateCancel;
}
}
}
var FiftyPenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 8) < 0.02 && Math.abs(coin.Diameter - 27.3) < 0.15)
{
alert("Captured 50p");
return salient.DelegateCancel;
}
}
}
var OnePoundHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 9.5) < 0.02 && Math.abs(coin.Diameter - 22.5) < 0.13)
{
alert("Captured £1");
return salient.DelegateCancel;
}
}
}
var CounterfeitCoinHandler = function()
{
this.handleCoin = function(coin)
{
alert("Coin is counterfeit");
}
}
var h5 = new FivePenceHandler();
var h10 = new TenPenceHandler();
var h20 = new TwentyPenceHandler();
var h50 = new FiftyPenceHandler();
var h100 = new OnePoundHandler();
var bogus = new CounterfeitCoinHandler();
var coinChainOfResponsibility = new salient.Delegate("CoinCOR");
coinChainOfResponsibility.addHandler(h5.handleCoin, h5);
coinChainOfResponsibility.addHandler(h10.handleCoin, h10);
coinChainOfResponsibility.addHandler(h20.handleCoin, h20);
coinChainOfResponsibility.addHandler(h50.handleCoin, h50);
coinChainOfResponsibility.addHandler(h100.handleCoin, h100);
coinChainOfResponsibility.addHandler(bogus.handleCoin, bogus);
var tenPence = new Coin(24.49, 6.5);
var fiftyPence = new Coin(27.31, 8.01);
var counterfeitPound = new Coin(22.5, 9);
coinChainOfResponsibility.invoke(tenPence);
coinChainOfResponsibility.invoke(fiftyPence);
coinChainOfResponsibility.invoke(counterfeitPound);
Alternately you could implement this pattern using return values. Handler return values are returned to the .invoke() call. If multiple values are returned, e.g. multiple handlers with return values, the .invoke() return value is an array.
Listing 4: Simple Chain Of Responsibility pattern example using return values
var Coin = function(diameter, weight)
{
this.Weight = weight;
this.Diameter = diameter;
}
var FivePenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 3.25) < 0.02 && Math.abs(coin.Diameter - 18) < 0.1)
{
return "5p";
}
}
}
var TenPenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 6.5) < 0.03 && Math.abs(coin.Diameter - 24.5) < 0.15)
{
return "10p";
}
}
}
var TwentyPenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 5) < 0.01 & Math.abs(coin.Diameter - 21.4) < 0.1)
{
return "10p";
}
}
}
var FiftyPenceHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 8) < 0.02 && Math.abs(coin.Diameter - 27.3) < 0.15)
{
return "50p";
}
}
}
var OnePoundHandler = function()
{
this.handleCoin = function(coin)
{
if (Math.abs(coin.Weight - 9.5) < 0.02 && Math.abs(coin.Diameter - 22.5) < 0.13)
{
return "£1";
}
}
}
var h5 = new FivePenceHandler();
var h10 = new TenPenceHandler();
var h20 = new TwentyPenceHandler();
var h50 = new FiftyPenceHandler();
var h100 = new OnePoundHandler();
var coinChainOfResponsibility = new salient.Delegate("CoinCOR");
coinChainOfResponsibility.addHandler(h5.handleCoin, h5);
coinChainOfResponsibility.addHandler(h10.handleCoin, h10);
coinChainOfResponsibility.addHandler(h20.handleCoin, h20);
coinChainOfResponsibility.addHandler(h50.handleCoin, h50);
coinChainOfResponsibility.addHandler(h100.handleCoin, h100);
var tenPence = new Coin(24.49, 6.5);
var fiftyPence = new Coin(27.31, 8.01);
var counterfeitPound = new Coin(22.5, 9);
alert(coinChainOfResponsibility.invoke(tenPence) || "bogus coin");
alert(coinChainOfResponsibility.invoke(fiftyPence) || "bogus coin");
alert(coinChainOfResponsibility.invoke(counterfeitPound) || "bogus coin");
NOTE: In that the Coin argument is an object it is being passed by reference and can me manipulated by each handler in turn. This enables the implementation of what I would like to call a Chain Of Custody pattern.
Command Pattern
Included is a simple interpretation of the Command pattern. The Command patterns allows loose coupling of a function and the code that invokes it. The target needs no foreknowledge of possible invokers and invokers can be added at any time and need no foreknowledge of the command they are meant to invoke other than the command key. Compare this to the Event pattern in which you explicitly bind a reference to a handler function to the event. Command pattern presents one more level of decoupling. Decoupling with Command pattern can be taken to the extreme of completely encapsulating the behaviour of an object, making private functions invocable via a command. This is a powerful concept that is not typically associated with JavaScript code.
A typical use case for a Command pattern can be inferred from any typical windows application. An application generally has some type of 'Save' functionality. You can generally invoke this functionality from an arbitrary number of places. e.g. Main Menu>File>Save, Toolbar>Save button, Keyboard> CTRL-S, etc.
Instead of writing code to hardwire each of these triggers to the Save function, we can use a Command pattern that drastically simplifies the architecture and implementation of an application.
I am explaining this pretty badly. Let me show some code.
Listing 5: salient.Commands API
var Commands = function()
{
}
Commands.prototype.publish = function(name)
{
}
Commands.prototype.unpublish = function(name)
{
}
Commands.prototype.subscribe = function(fn, context, name)
{
}
Commands.prototype.unsubscribe = function(name, handle)
{
}
Commands.prototype.invoke = function(name, sender, args)
{
}
Commands.prototype.clear = function()
{
}
Listing 6: A simple Command pattern example
var myAppCore = new function()
{
var commands = this.commands = new salient.Commands();
function save(sender, args)
{
alert("I saved " + args);
}
commands.subscribe(save, this, "SAVE");
}
var myAppSaveButton =
{
click: function()
{
myAppCore.commands.invoke("SAVE", this, "name of doc (invoked via button)");
}
}
var myAppKeyboardSave=
{
press: function()
{
myAppCore.commands.invoke("SAVE", this, "name of doc (invoked via keyboard)");
}
}
myAppSaveButton.click();
myAppKeyboardSave.press();
This implementation publishes core functionality, Save, as a command named "SAVE". If an invocation is attempted on a non existant command, an exception is thrown.
A further level of decoupling can be accomplished by simply publishing a command in the application scope. You can then attach/detach subscribers at will. Invocation of commands that are published but have no subscribers results in a noop.
In that the Command pattern is especially useful in a single threaded runtime like JavaScript I will revisit this subject and present a more formal implementation of the Command pattern, including command journalling to enable 'undo'/'redo' in a later article.
Implementation
salient.Delegate, at it's core, is a variation of a Chain of Responsibility pattern in that it is basically an array of context objects (handlers) that are called in sequence upon invocation of the delegate. In addition to the standard behaviour you would expect from an event, the addition of return value(s), arbitrary numbers of arguments, arbitrary scoping and the capability of cancelling the invocation chain leads me to characterize it as a Delegate for lack of a better term.
Initially, the invocation chain was implemented as a simple iteration of an array of callbacks. While this works quite well when everything is in it's proper place, an error during the invocation of a handler leaves us with 2 options: silently swallow the error and continue the invocation chain or let the error propigate and break the invocation chain.
What I really had in mind was a more robust behaviour more closely resembling the native handling of events, in that an error can occur in one handler that can be handled or not but does not affect the invocation of any subsequent handlers.
I struggled with this for quite some time until I happened to visit Dean's blog and read this post, Callbacks vs Events, in which he describes a means of wrapping a callback in the native event model. Exactly what I was after. With a not-too-significant degree of adaptation I was able to implement his idea without altering the advertised behavior or API of salient.Delegate.
Listing 7: salient.Delegate and salient.Commands source
var salient = {};
(function()
{
var DelegateContext = function(fn, context, token)
{
this.fn = fn;
this.token = token;
this.context = context;
};
DelegateContext.prototype.destroy = function()
{
this.fn = null;
this.token = null;
this.context = null;
return null;
};
var Delegate = function(name)
{
if (name instanceof salient.Delegate)
{
return name;
}
this.name = name;
this.id = delegate_guid++;
this.handlers = [];
};
Delegate.prototype.addHandler = function(fn, context)
{
var handler = this.find(fn);
if (!handler)
{
delegate_guid++;
var token = this.name + "_" + this.id + "_" + delegate_guid;
handler = new DelegateContext(fn, context, token);
this.handlers.push(handler);
}
return handler.token;
};
Delegate.prototype.removeHandler = function(handle)
{
var i = this.indexOf(handle);
if (i < 0)
{
throw new Error("handle does not exists");
}
var handler = this.handlers[i];
handler = handler.destroy();
this.handlers[i] = null;
};
Delegate.prototype.invoke = function()
{
var results = [];
for (var i = 0; i < this.handlers.length; i++)
{
var handler = this.handlers[i];
if (handler !== null)
{
handler.args = arguments;
currentHandler = handler;
dispatchFakeEvent();
if (handler.result instanceof DelegateCancellation)
{
break;
}
if (typeof (handler.result) !== "undefined")
{
results.push(handler.result);
}
}
}
return results.length === 0 ? undefined : (results.length === 1 ? results[0] : results);
};
Delegate.prototype.destroy = function()
{
for (var i = 0; i < this.handlers.length; i++)
{
var handler = this.handlers[i];
handler = handler.destroy();
}
this.handlers = [];
return null;
};
Delegate.prototype.count = function()
{
return this.handlers.length;
};
Delegate.prototype.indexOf = function(handle)
{
for (var i = 0; i < this.handlers.length; i++)
{
if (this.handlers[i] === null)
{
continue;
}
if (this.handlers[i].token === handle || this.handlers[i].fn === handle)
{
return i;
}
}
return -1;
};
Delegate.prototype.find = function(handle)
{
var i = this.indexOf(handle);
if (i > -1)
{
return this.handlers[i];
}
};
var delegate_guid = 1;
var DelegateCancellation = function()
{
}
var currentHandler;
if (document.addEventListener)
{
document.addEventListener("___salient_delegate", function()
{
var handler = currentHandler;
handler.result = handler.fn.apply(handler.context, handler.args);
}, false);
var dispatchFakeEvent = function()
{
var fakeEvent = document.createEvent("UIEvents");
fakeEvent.initEvent("___salient_delegate", false, false);
document.dispatchEvent(fakeEvent);
};
}
else
{
function initEvents()
{
document.documentElement.___salient_delegate = 0;
document.documentElement.attachEvent("onpropertychange", function()
{
if (event.propertyName == "___salient_delegate")
{
var handler = currentHandler;
handler.result = handler.fn.apply(handler.context, handler.args);
}
});
dispatchFakeEvent = function(handler)
{
document.documentElement.___salient_delegate++;
};
}
try { initEvents(); } catch (ex) { }
}
var Commands = function()
{
this._events = {};
}
Commands.prototype.publish = function(name)
{
if (!this._events[name])
{
this._events[name] = new salient.Delegate(name);
}
}
Commands.prototype.unpublish = function(name)
{
if (this._events[name])
{
this._events[name] = this._events[name].destroy();
}
}
Commands.prototype.subscribe = function(fn, context, name)
{
if (!this._events[name])
{
this.publish(name);
}
return this._events[name].addHandler(fn, context);
}
Commands.prototype.unsubscribe = function(name, handle)
{
if (this._events[name])
{
if (handle)
{
this._events[name].removeHandler(handle);
}
else
{
this._events[name].destroy();
}
}
}
Commands.prototype.invoke = function(name, sender, args)
{
var cmd = this._events[name];
if (!cmd)
{
throw new Error(0, name + " is not a registered command");
}
cmd.invoke(sender, args);
}
Commands.prototype.clear = function()
{
for (var name in this._events)
{
if (this._events.hasOwnProperty(name))
{
this._events[name] = this._events[name].destroy();
}
}
}
this.DelegateCancel = new DelegateCancellation();
this.Delegate = Delegate;
this.DelegateContext = DelegateContext;
this.Commands = Commands;
Commands.__class = true;
Commands.__typeName = "salient.Commands";
Delegate.__class = true;
Delegate.__typeName = "salient.Delegate";
DelegateContext.__class = true;
DelegateContext.__typeName = "salient.DelegateContext";
this.__namespace = true;
this.__typename = "salient";
}).call(salient);