Evented programming patterns: Observable objects

This post is part of a series on event-driven programming. The complete series is:

As I mentioned in the previous article, events are not things that only live in the DOM. An event is simply a point in time at which a change happens: the user clicks something, an animation completes, a piece of data changes. Events are everywhere in GUI programming and also crop up in various server-side applications, especially with the rise of Node.js. Making your APIs support events is a great way to build modular applications and keep your code maintainable and extensible at the same time.

Observable objects are simply objects that can publish events. Other parts of an application can listen to the object and execute additional code whenever the object changes in some way. This means the object can have additional functionality attached to its behaviour without modifying its source code. As a concrete example, I’m going to show how I’d implement Yehuda’s tabs example from yesterday’s post in an object-oriented style.

The markup and setup code in my implementation will look very similar:

<ul class="tabs">
  <li data-pane="first">First</li>
  <li data-pane="second" class="selected">
    Second
  </li>
  <li data-pane="third">Third</li>
</ul>

<div class="pane" id="first">Some content</div>
<div class="pane" id="second">Some content</div>
<div class="pane" id="third">Some content</div>

<script type="text/javascript">
  var tabs = new Tabs("ul.tabs");
  tabs.on("change", function(index) {
    tabs.getPanes().hide()
         .at(index).show();
  });
  tabs.select(0);
</script>

This probably looks superficially similar to the first implementation. The difference is that in this example, other scripts that want to interact with the tabs object have a proper API for doing so, so it’s clearer what behaviour is deliberate public API and what is implementation detail. In the previous version, not being able to tell the abstraction from the implementation (since it all looks like DOM code) could create serious maintenance problems later on. Our Tabs will look like this: (I’m using Ojay for this example. It has its own Observable module but we’ll ignore that for now.)

Tabs = new JS.Class({
  initialize: function(selector) {
    this._tabs = Ojay(selector).children();

    this._tabs.forEach(function(tab, index) {
      tab.on("click", function() { this.select(index) }, this);
    }, this);
  },

  select: function(index) {
    this._selectedIndex = index;
    this._tabs.removeClass("selected");
    this._tabs.at(index).addClass("selected");
    this.trigger("change", [index]);
  },

  getPanes: function() {
    var ids = this._tabs.map(function(tab) {
      return "#" + tab.node.getAttribute("data-pane");
    });
    return Ojay.apply(this, ids);
  }
});

It’s fine to have user interface widgets implemented as objects like this; in fact it can make them easier to reuse and glue together as long as they have a decent event API. Speaking of which, you’ll notice that Tabs is missing two methods our implementation depends on: on() and trigger(). These are the bedrock of any observable object, allowing other parts of the system to add listeners and allowing the object itself to notify listeners of changes. There are many ways to implement these depending on your needs, but a good starting point could look like this:

Observable = {
  on: function(eventType, listener, scope) {
    this._listeners = this._listeners || {};
    var list = this._listeners[eventType] = this._listeners[eventType] || [];
    list.push([listener, scope]);
  },
  
  trigger: function(eventType, args) {
    if (!this._listeners) return;
    var list = this._listeners[eventType];
    if (!list) return;
    list.forEach(function(listener) {
      listener[0].apply(listener[1], args);
    });
  }
};

Observable.on() takes three arguments: the name of the event, a listener function, and (optionally) a scope that this will be bound to when the listener is called. (The scope argument is useful in any JavaScript API method that takes a callback function, making it easier to use the method in an object-oriented context.) All that on() does is store the listener in a collection (this._listeners) indexed by event type.

Observable.trigger() is the other side of the bargain: it lets the object send notifications that events are taking place. First it looks to see whether there are any listeners for the triggered event, and if there are then it calls each of them with the args the event was triggered with. In our case our change event sends the newly selected index to all the listeners as this is a useful piece of information about the event that listeners are likely to use. Our application above uses the index to hide and show a series of content panes.

We can take these methods and add them to any class we like since they’re totally generic. In Ojay this is done like this:

Tabs.include(Observable);

So when should you use an observable object? The short answer is, any time you want a change to have consequences that aren’t directly related to the class at hand. The canonical example is logging, for example we may decide we want to run analytics when the tab selection changes but don’t want to bloat the class up with tracking code:

tabs.on("change", function(index) {
  pageTracker._trackPageview("/events/tabchange/" + index);
});

(See also the Facebook examples in yesterday’s article.) Think of trigger() as an extension point that opens the class up to accept new behaviour without having its source modified. The listeners you add to an object ought to be independent; the idea of the observable object is to provide a way of glueing together modules without tightly coupling them. One common gotcha (as illustrated by Yehuda’s demo) is adding listeners that depend on each other:

$("ul.tabs").bind("change", function() {
  $(this).attr("panes").hide();
  $(this).attr("selected").show();
});

$("ul.tabs").bind("change", function(e) {
  $(this).attr("selected", e.target);

  $(e.target)
    .addClass("selected")
    .siblings()
    .removeClass("selected");
});

These two listeners are bound to the same event, yet one reads and one writes the value of $(this).attr("selected"). This relies on the listeners being executed in a certain order, which is an anti-pattern in any asynchronous or event-driven system. Separate listeners ought not to know about each others’ existence, so if you have two listeners that depend on each other consider merging them into one atomic function to make sure they work correctly, or find another way to decouple them.

This piece also illustrates a more general approach that I take to building GUIs. I provide an API for scripting every atomic action the widget can perform, for example the Tabs#select(index) method. This allows multiple scripts and UI elements to control the widget. Then I wire up the UI elements in the DOM with very thin event listeners that call this API, for example

    this._tabs.forEach(function(tab, index) {
      tab.on("click", function() { this.select(index) }, this);
    }, this);

The widget’s methods perform the absolute core of required behaviour before firing any relevant events to allow other parts of the application perform additional actions. This takes a little discipline to stick to but it’s usually straightforward and yields modular, flexible and easily extensible code if you get it right.

Up next: waiting for responses with the Deferrable pattern.