Creating Objects with Observable Properties in JavaScript
Posted by: Clarity Blogs: ASP.NET,
on 21 Aug 2009 |
View original | Bookmarked: 0 time(s)
Regular readers will notice that Im quite taken with JavaScript recently. Ive been looking at using it to build a really simple role playing game for the browser. Really old school. Think Dragon Warrior. Ive just gotten started, but Im 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 its 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, lets define observable properties. If youre 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 instances 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 raise change notifications on. Id have to type out that same blob of repetitive and error prone logic for each one, and I cant 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 heros stats and displaying them, but I also want to have a console logging out events. Or any other number of use cases: Ill probably want to watch the heros 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 theyre low on health.
So I need observable properties. And I definitely dont need to write seven lines of code per property per object for every single thing ever. Ideally, Id 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 youd 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. Heres the code for it, though I wont 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, its 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 youll 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. Heres 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 theres 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, lets flesh out createObservable. Heres 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. Lets 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 its just going to be a thin wrapper over our notifiers subscribe function. Lets 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
};
Im a little bit lukewarm on doing this. My gut tells me the more verbose form is clearer, so Im going to leave things spelled out. (Also, if youre in an environment with intellisense, its 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 doesnt really answer our question, since now we need to define createObservableProperty. Lets 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 youve been paying attention, you know that the way we maintain this kind of private state is with a closure. Heres 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). Lets follow along. First, note that propName and value are both tied up into this closure. This means theyre 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 its 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 youll 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! Heres 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 were still not doing anything with the properties passed in! Less than ideal. Well register them all at once just before returning:
for (propName in properties) {
observable.register(propName, properties[propName]);
}
Pretty simple. Theres one more thing Im going to add, but its 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, were 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 dont 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.
Lets 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. Were 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 heros 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. Lets 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 heros relevant stats. Notice that were 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. Were 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 closureswe want statName as it is now, not as it will be at the end of the iteration.)
Thats pretty much it! Lets fire it up and prove that it works:
Hope this helps.