Search code examples
javascriptdesign-patternsobserver-patternobservers

JavaScript: Why so much abstraction/interfacing in Addy's Observer Pattern?


I'm working through the design pattern example for the Observer Pattern in Addy Osmani's book, "JavaScript Design Patterns". My question is why is it important that there are so many levels of abstraction in his implementation of the pattern?

For instance, in his example, just to add an observer ('push' an observer to an array), this involves:

  • using the native push() method.
  • creating a ObjectList.add() method.
  • inheriting/extending the ObjectList object with the Subject object.
  • creating the Subject.addObserver() method which to be used as an interface, but which uses the ObjectList.add method under the hood.
  • extending the Subject.addObserver() method for new objects.
  • Implementing it by calling the addObserver() method on the newly extended object.

Here's the code example for the design pattern in its entirety:

function ObserverList(){
  this.observerList = [];
}

ObserverList.prototype.add = function( obj ){
  return this.observerList.push( obj );
};

ObserverList.prototype.count = function(){
  return this.observerList.length;
};

ObserverList.prototype.get = function( index ){
  if( index > -1 && index < this.observerList.length ){
    return this.observerList[ index ];
  }
};

ObserverList.prototype.indexOf = function( obj, startIndex ){
  var i = startIndex;

  while( i < this.observerList.length ){
    if( this.observerList[i] === obj ){
      return i;
    }
    i++;
  }

  return -1;
};

ObserverList.prototype.removeAt = function( index ){
  this.observerList.splice( index, 1 );
};

function Subject(){
  this.observers = new ObserverList();
}

Subject.prototype.addObserver = function( observer ){
  this.observers.add( observer );
};

Subject.prototype.removeObserver = function( observer ){
  this.observers.removeAt( this.observers.indexOf( observer, 0 ) );
};

Subject.prototype.notify = function( context ){
  var observerCount = this.observers.count();
  for(var i=0; i < observerCount; i++){
 this.observers.get(i).update( context );
  }
};

// The Observer
function Observer(){
  this.update = function(){
    // ...
  };
}

And here's the implementation/usage:

HTML

<button id="addNewObserver">Add New Observer checkbox</button>
<input id="mainCheckbox" type="checkbox"/>
<div id="observersContainer"></div>

Script

// Extend an object with an extension
function extend( extension, obj ){
  for ( var key in extension ){
    obj[key] = extension[key];
  }
}

// References to our DOM elements

var controlCheckbox = document.getElementById( "mainCheckbox" ),
  addBtn = document.getElementById( "addNewObserver" ),
  container = document.getElementById( "observersContainer" );


// Concrete Subject

// Extend the controlling checkbox with the Subject class
extend( new Subject(), controlCheckbox );

// Clicking the checkbox will trigger notifications to its observers
controlCheckbox.onclick = function(){
  controlCheckbox.notify( controlCheckbox.checked );
};

addBtn.onclick = addNewObserver;

// Concrete Observer

function addNewObserver(){

  // Create a new checkbox to be added
  var check  = document.createElement( "input" );
  check.type = "checkbox";

  // Extend the checkbox with the Observer class
  extend( new Observer(), check );

  // Override with custom update behaviour
  check.update = function( value ){
    this.checked = value;
  };

  // Add the new observer to our list of observers
  // for our main subject
  controlCheckbox.addObserver( check );

  // Append the item to the container
  container.appendChild( check );
}

Now I compared his implementation with other implementations of the same pattern (books and blogs). And it seems that Addy adds a ton more abstraction than other implementors of the observer pattern. The question is, why? Couldn't this be implemented more simply by inheriting from the ObserverList object? Does this achieve a greater degree of decoupling doing it the way Addy does? If so, how exactly is that? Doesn't the design pattern itself create a decoupling? Seems like the Subject object brings a lot of unnecessary code.


Solution

  • Couldn't this be implemented more simply by inheriting from the ObserverList object?

    Yes. By inheriting, there would be no reimplementing of all the ObserverList methods. Signficantly less code, less testing and less documentation.

    Does this achieve a greater degree of decoupling doing it the way Addy does?

    Yes, it does because the interface to the Subject object is not dependent at all on the ObserverList interface (because Subject has re-implemented its own interface to those methods so its interface is decoupled from the ObserverList interface. This has its pros and its cons. Reimplementing an interface should only be done with good reason because its mostly just a bunch of extra code that adds no actual useful functionality.

    If so, how exactly is that?

    Hiding the actual interface to the ObserverList by reimplementing your own version decouples the two interfaces. A change to the underlying ObserverList interface can be hidden by the Subject interface. While the Subject implementation is still dependent and coupled to the ObserverList interface, the Subject interface itself is independent from the ObserverList interface. But, there are plenty of reasons to not do this too so don't go thinking that every interface should be decoupled from every other interface. That would be a disaster to follow that everywhere.

    Seems like the Subject object brings a lot of unnecessary code.

    Yes, it does.


    When you want to use functionality from another object and you want to expose some or all of that functionality to the customers of your own object, you have a number of design choices.

    1. Your object can inherit from that other object, thus exposing its entire interface automatically (and allowing you to override some methods if desired).

    2. Your object can contain an instance of that other object and expose that object publicly so that users of your object can get direct access to the other object without you reimplementing anything. This would probably be my choice in this particular case so code using the publicly available observer within a Subject object would look like this:

      var s = new Subject(); s.observer.add(function() { // this gets called when subject is changed });

    3. Your object can contain a private instance of that other object and you manually create your own interface on top of that private instance. This is what the code in your book is doing.

    In OO-speak, these three choices are sometimes referred to as isA, hasA and hidesA. In the first case your object "is a" ObserverList object. In the second case, your object "has a" ObserverList object. In the third case, your object "hides a" ObserverList object inside its implementation.


    There are pros and cons to each design choice. None of the choices is always the right or wrong way to do things as each has different pros/cons and which is the optimal choice depends upon the situation.

    The case for option 1) inheriting is generally when your object is an extenstion of the base object and architecturally speaking it is thought of as just a more powerful version of the base object and/or it may override methods on the base object. That isn't really the case here. A Subject() object isn't a more powerful ObserverList object. It's a different type of object that happens to use an ObserverList.

    The case for option 2) of containing a public instance of an ObserverList and letting users of your object use that public instance is when your object is really a different kind of object, but it wants to use and expose to its users the functionality of another type of object. That seems to me mostly what is happening here.

    The case for option 3) is when you do not want any interface dependency between the interface to your object and any other interfaces. In that case, you can't expose some other object's interface to let users of your object use those. Instead, you have to cover any other interfaces with your own. This means more code, more testing, more documentation and more maintenance. But, a change in the underlying interface you are using does not necessarily cause a change to your own interface. You have the option of hiding any underlying changes in interface (at the cost of a lot more work). But, while gaining control, you also have more work. If the ObserverList object adds three new methods, in the other two options those methods are immediately available to your users without you having to do any new work, but in option 3), you have to create new cover methods for them and test and document those before they can be available to your clients.