Creating Objects with Observable Properties in JavaScript
Regular readers will notice that I’m quite taken with JavaScript recently. I’ve been looking at using it to build a really simple role playing game for the browser. Really old school. Think Dragon Warrior. I’ve just gotten started, but I’m already so impressed by the power and flexibility of the language that I had to get on here and yammer about it for awhile.
One of the core ideas in JavaScript is that every object is a hash, a collection of name-value pairs. This is a really simple and powerful concept, but it’s not always clear how to set up more advanced behaviors. For example, I want to have the notion of observable properties in my game. First, let’s define “observable properties.” If you’re familiar with C#, think about the INotifyPropertyChanged interface:
public class Actor : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
OnPropertyChanged("Name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
So consumers can hook up to an instance’s PropertyChanged event and get notified when something changes. As a side note, look how much it sucks to do this in C#. Suppose I had several properties to raise change notifications on. I’d have to type out that same blob of repetitive and error prone logic for each one, and I can’t really see any good way of compressing this. Why do I have to write so much code to express such a simple idea? I digress.
Anyway, I think this will allow me to factor the game logic cleanly, instead of having a battalion of special cases built into my game engine. Maybe I want to have a status table monitoring the hero’s stats and displaying them, but I also want to have a console logging out events. Or any other number of use cases: I’ll probably want to watch the hero’s hit points and end the game if they ever turn to zero. Maybe some monsters will observe their own hit points and behave differently when they’re low on health.
So I need observable properties. And I definitely don’t need to write seven lines of code per property per object for every single thing ever. Ideally, I’d like to be able to say something like this:
var hero = createObservable({
name: 'ERDRICK',
hitPoints: 10,
strength: 18,
agility: 12
});
And have it work the way you’d expect, registering all the supplied properties (with the specified defaults) as observable on our new object. So how can we do this? Well, first we need to pull out the mediator/event aggregator/event hub/whatever I talked about here. Here’s the code for it, though I won’t go into detail explaining it here:
var createMediator = function () {
var events = {};
return {
subscribe: function (eventName, callback) {
events[eventName] = events[eventName] || [];
events[eventName].push(callback);
},
publish: function (eventName) {
var i, callbacks = events[eventName], args;
if (callbacks) {
args = Array.prototype.slice.call(arguments, 1);
for (i = 0; i < callbacks.length; i++) {
callbacks[i].apply(null, args);
}
}
}
}
};
Calls to createMediator will give us a fresh mediator for subscribing to and publishing events. This is key because each observable object is going to need its own mediator. Now we need to flesh out our createObservable function. To understand it, it’s going to be critical to understand how closures work.
In JavaScript, functions are values. We can pass them around, create new ones inline, and return them from other functions. The last one is really key, and you’ll see why in a second. When we create a function inline, it creates a new scope, but it can still refer to objects from the surrounding scope. Doing so creates a closure. Here’s an example:
var createClosure = function (param) {
return function () {
return param;
};
};
var closure = createClosure('stuff');
createClosure is a function that returns a trivial function. Notice that the inner function we return outlives its parent scope. This means that, while param goes out of scope, it remains alive because the function returned holds a reference to it. Notice also that there’s no way we can change param through our closure object. closure is totally opaque to consumers. This means we can use closures to create private attributes in an object. Hopefully this makes sense; if not, a better explanation can be found here.
Ok, let’s flesh out createObservable. Here’s a shell:
var createObservable = function (properties) {
var notifier = createMediator(), observable;
return observable;
};
We have our notifier, which is an instance of our mediator, and we have our observable object, which is currently nothing. Let’s build this out more by defining an interface to an observable object. There are really two things we want to do: register observable properties and observe existing ones.
var createObservable = function (properties) {
var notifier = createMediator(), observable;
observable = {
register: function () {
// ???
},
observe: function () {
// ???
}
};
return observable;
};
observe should be pretty obvious, since it’s just going to be a thin wrapper over our notifier’s subscribe function. Let’s fill that in now.
observable = {
register: function () {
// ???
},
observe: function (propName, observer) {
notifier.subscribe(propName, observer);
}
};
Note that, since functions are simply values in JavaScript, we could just say this:
observable = {
register: function () {
// ???
},
observe: notifier.subscribe
};
I’m a little bit lukewarm on doing this. My gut tells me the more verbose form is clearer, so I’m going to leave things spelled out. (Also, if you’re in an environment with intellisense, it’s nice to have the semantics of propName and observer as your parameter names rather than eventName and callback.)
What about register? register is more complicated. We want something like this:
observable = {
register: function (propName, value) {
this[propName] = createObservableProperty(propName, value);
},
observe: function (propName, observer) {
notifier.subscribe(propName, observer);
}
};
Knowing that all objects in JavaScript are hashes, we hash our object on propName and set it equal to an observable property. Of course, this doesn’t really answer our question, since now we need to define createObservableProperty. Let’s take a look at the big picture:
var createObservable = function (properties) {
var notifier = createMediator(), createObservableProperty, observable;
createObservableProperty = function (propName, value) {
// ???
};
observable = {
register: function () {
this[propName] = createObservableProperty(propName, value);
},
observe: function (propName, observer) {
notifier.subscribe(propName, observer);
}
};
return observable;
};
We need to fill out createObservableProperty such that it returns something that knows how to handle property change notifications. If you’ve been paying attention, you know that the way we maintain this kind of private state is with a closure. Here’s our implementation:
createObservableProperty = function (propName, value) {
return function (newValue) {
var oldValue;
if (typeof newValue !== 'undefined' &&
value !== newValue) {
oldValue = value;
value = newValue;
notifier.publish(propName, oldValue, value);
}
return value;
};
};
This function we return is going to act as both a getter and setter (depending on whether or not the optional newValue parameter is passed in). Let’s follow along. First, note that propName and value are both tied up into this closure. This means they’re effectively private variables for our observable property. Our get/set function first checks to see if newValue was passed in (by checking against undefined) and then whether it’s a different value. If it is, we save the old value to a temp variable, set the new value, and then publish an event via our notifier specifying the property name and the old and new values. (If you’ll recall, callbacks to our events can care/not care about those passed parameters as they wish.) We end by returning the value.
This is pretty much all we need! Here’s the complete implementation:
var createObservable = function (properties) {
var notifier = createMediator(), createObservableProperty, observable;
createObservableProperty = function (propName, value) {
return function (newValue) {
var oldValue;
if (typeof newValue !== 'undefined' &&
value !== newValue) {
oldValue = value;
value = newValue;
notifier.publish(propName, oldValue, value);
}
return value;
};
};
observable = {
register: function (propName, value) {
this[propName] = createObservableProperty(propName, value);
},
observe: function (propName, observer) {
notifier.subscribe(propName, observer);
}
};
return observable;
};
This is really all we need to get going, but you may have noticed that we’re still not doing anything with the properties passed in! Less than ideal. We’ll register them all at once just before returning:
for (propName in properties) {
observable.register(propName, properties[propName]);
}
Pretty simple. There’s one more thing I’m going to add, but it’s not required. It might be nice (for various reasons) to know what observable properties an object has. Adding this is easy:
observable = {
register: function (propName, value) {
this[propName] = createObservableProperty(propName, value);
this.observableProperties.push(propName);
},
observe: function (propName, observer) {
notifier.subscribe(propName, observer);
},
observableProperties: []
};
observableProperties is an array. When we register a new property, we push propName onto the array.
Now, as always, we’re going to wrap everything into a big global object to minimize our footprint on the global namespace. Like so:
var DW = function () {
var createMediator, createObservable;
createMediator = function () {
// ...
};
createObservable = function (properties) {
// ...
};
return {
log: function () {
var line = $('<div></div>');
return function (message) {
$('#console').append(
line.clone().text(message)
);
};
}(),
hero: createObservable({
name: 'ERDRICK',
strength: 18,
agility: 12
})
};
}();
Notice that we don’t even expose our createMediator or createObservable objects. We only expose log, a function for writing messages to our console (note the use, as always, of the incomparable jQuery), and hero, our dude with observable properties name, strength, and agility. Notice how easy it is to create observable objects in a clean and concise way, and then go back to the beginning of this post and compare it to the lengths C# makes you go. Absurd.
Let’s add some code on our page to make use of all this nifty stuff.
$(function () {
DW.hero.observe('strength', function (oldValue, newValue) {
DW.log('courage and wit have served thee well! thy strength increases from ' + oldValue + ' to ' + newValue);
});
DW.hero.observe('agility', function (oldValue, newValue) {
DW.log('courage and wit have served thee well! thy agility increases from ' + oldValue + ' to ' + newValue);
});
$('#incStrength').click(function () {
DW.hero.strength(DW.hero.strength() + 2);
});
$('#incAgility').click(function () {
DW.hero.agility(DW.hero.agility() + 3);
});
});
This code is really simple. We’re hooking up a couple observers to the strength and agility properties that will simply log messages to the console when the property changes. We then hook up some click handlers to buttons to increase the hero’s strength and agility by arbitrary amounts. Note the use of the observable properties in the click handlers both as getters (to find the current value) and setters. Let’s add some more observers, just to prove we can have lots of people watching at once. This is going to be a little more complicated, but not too bad (I hope):
var i, statName, stats;
stats = DW.hero.observableProperties;
for (i = 0; i < stats.length; i++) {
statName = stats[i];
$('#status').append(
$('<div></div>')
.attr('id', statName)
.text(statName + ': ' + DW.hero[statName]())
);
DW.hero.observe(statName, function (statName) {
var selector = '#' + statName;
return function (oldValue, newValue) {
$(selector).text(statName + ': ' + newValue);
};
}(statName));
}
(This is all still in the jQuery document.ready handler.) This is basically to set up a status area to tell us the hero’s relevant stats. Notice that we’re using observableProperties to get all the relevant properties. We basically set up observers for each one to update the stats. The call to observe is a little complicated. We’re actually passing in the returned function as our parameter. (Notice the immediate invocation of the wrapping function with parameter statName. This is done to avoid a common gotcha with closures—we want statName as it is now, not as it will be at the end of the iteration.)
That’s pretty much it! Let’s fire it up and prove that it works:
Hope this helps.