Javascript – Observer Pattern Implementation

While working on a project that needs to notify when updates are made, I started playing with the Observer Pattern in Javascript. Changes made to the Observed Object trigger a console.log when updates are made. The actual implementation is using SocketIO to notify users of updates concurrent with updates to the persistence layer.

Note: The code is still in development and needs additional refactoring.

(function () {

  'use strict';
  /*global console */

  function Observable() {

  // Observable class
    var _observingArray = [];
    
    return {

      action: function (action, func) {
        console.log(action, func);
      },

      addWatcher: function (obj) {
        var funcName = arguments[0].name;
        if (!_observingArray[funcName]) {
          this.action('New Watcher:', funcName);
          _observingArray.push(obj);
        }
      },
      
      removeWatcher: function(obj) {
        var i = 0,
          len = _observingArray.length;
        for (i; i < len; i++ ) {
          if (_observingArray[i] === obj ) {
            _observingArray.splice(i, 1);
            var funcName = arguments[0].name;
            this.action('Removing Watcher:', funcName);
            return true;
          }
        }
        return false;
      },
      
      notify: function () {
        // arguments is an array like object / converted to array here
        var args = Array.prototype.slice.call( arguments, 0 );
        var i = 0,
          len = _observingArray.length;
        for (i; i < len; i++) {
          _observingArray[i].update.apply( null, args );
        }
      }

    };
  }

  // basic example of observer object with 
  function MakeObservableObject() {

    // very basic object comparator
    function updateObject(obj, updates) {
        var updated = false;

        Object.keys(updates).forEach(function(k) {
          // if property values are different update
          if (obj.contents[k] !== updates[k]) {
            if (updates[k] > 0 || updates[k].length > 0) {
              // if property has a value - update
              obj.contents[k] = updates[k];
            } else {
              // if property has no value - delete the property
              delete obj[k];
            }
            updated = true;
          }
        });
        if (updated) {
          return obj;
        }
        return false;
    }

    var items = {}; 

    var observer = MakeObserver(this);

    // notifies watchers
    this.getItems = function getItems() {
      observer.notify(items);
    };

    // initial configuration of object properties
    // does not notify watchers
    this.setItems = function setItems(obj) {
      items = obj;
    };

    // updates object properties
    // notifies watchers of new object contents
    this.update = function updateItems(updates) {
      if (updates) {
        var updated = updateObject(items, updates);
        if (updated) {
          items = updated;
          observer.notify(items);
        }
      }    
    };
  }
  

  function MakeObserver(obj) {
      
      // obj passed in is extended with Observable methods and returned
      var observable = Observable();

      // obj passed in is added as an observer
      obj.addObserver = function addObserver(observer) {
        observable.addWatcher(observer);
      };

      // obj passed in is removed from observable as an observer
      obj.removeObserver = function removeObserver(observer) {
        observable.removeWatcher(observer);
      };

      return observable;
  }

  // ============================
  // ========= Examples =========
  // ============================

  // CUSTOM OBSERVER Methods
  var ItemUpdater = {
    name: 'ItemUpdater',
    update : function() {
      var item = arguments[0];
      console.log( 'Update ItemUpdater:', item.name, ' = ', JSON.stringify(item.contents));
    }
  };

  var ItemCharts = {
    name: 'ItemCharts',
    update : function() {
      var item = arguments[0];
      console.log( 'Update ItemCharts:', item.name, ' = ', JSON.stringify(item.contents));
    }
  };

  // OBSERVABLE OBJECT
  var app = new MakeObservableObject();

  // OBSERVABLE OBJECT CONTENTS
  var items = {
    name: 'clothes', 
    'contents': {socks : 7, pants : 3, shirts : 4}
    }; 

  // SET OBSERVABLE OBJECTS - PUBLIC CONTENTS 
  app.setItems.call(app, items);

  // ADD OBSERVER
  app.addObserver(ItemUpdater);

  // ADD OBSERVER
  app.addObserver(ItemCharts);

  // CHECK CURRENT CONTENTS / TRIGGER NOTIFICATION
  app.getItems();

  // REMOVE OBSERVER
  app.removeObserver(ItemUpdater);

  // REMOVE OBSERVER
  app.removeObserver(ItemCharts);

  // ADD OBSERVER
  app.addObserver(ItemCharts);
  app.update({'ties':26.00});
  app.getItems();

  // OBSERVABLE OBJECT
  var music = new MakeObservableObject();
  // OBSERVABLE CONTENTS
  var albums = {
    name: 'music', 
    'contents': {blues : 1, jazz : 3, punk : 2}
    }; 
  music.setItems(albums);
  music.addObserver(ItemCharts);
  music.addObserver(ItemUpdater);

}());

Leave a Reply

Your email address will not be published. Required fields are marked *