Introduction

In this post I’m going to describe  how cyclic dependencies can cause headache after long hours of debugging and how to solve them using publisher-subscriber design pattern in form of very simple message relay/bus.

I’ll stick to simple environment:

  • jquery 1.10
  • requirejs 2.1
  • jasminejs 1.3.1

There will be some optional dependencies introduced on the way:

  • phantomjs
  • nodejs
  • grunt
  • madgen

After presenting solution I’m going to present a working example with interchangable parts on both notifier and listener side.

Problem description

So there you have it – you built your application using requirejs where modules are decomposed using MVC pattern – model is managed in the DAO layer which communicates with online (remote) and offline (local) database. Configuration for both of them is stored locally in Setting module and at some point Settings got saved in the database which introduced cyclic dependency:

DAO <-> Settings

So the only way to deal with it in current architecture was to give DAO knowledge about Settings and Settings about DAO. This leads to a very complex application structure, which may also be called everyone-knows-everyone.

e.g.


define("settings", ["fs", "dao"], function(fs, dao) {

  var fsSettings = fs.read("settings.json");
  // eval it
  var settings = Object.create(fsSettings, dao.read("settings"));

  function read(key) {
   return settings[key];
  }

  return {
    read: read
  };
});

define("dao", ["settings"], function(settings) {

  function Dao(url){}

  Dao.prototype.init = function daoInit(url) {};

  var dao = new Dao(settings.get('url'));

});

RequireJS solution

You could use special exports keyword (described here) but this means that your modules preserve tight coupling anti-pattern.

Publisher/subscriber – message bus

Publisher subscriber is a design pattern that allows low coupling and high cohesion between different modules of the system. A simple requirements written in BDD/jasmine style would look like this (included below):

describe("message bus", function() {

  it("should allow adding listener for event", function() {

    mBus.addEventListener("myEvent", function(){});

    expect(mBus.length("myEvent")).toEqual(1);

  });

  it("should allow removing listener for event", function() {

    var fn = function(){};

    mBus.addEventListener("myEvent", fn);

    expect(mBus.length("myEvent")).toEqual(1);

    mBus.removeEventListener(fn);

    expect(mBus.listeners("myEvent").length).toEqual(0);

  });

  it("should allow broadcast", function() {

    var fn = jasmine.createSpy('fn'),

    o =  {prop:11};

    mBus.addEventListener("myEvent", fn);

    expect(mBus.length("myEvent")).toEqual(1);

    mBus.notify("myEvent", o);

    expect(fn.calls.length).toEqual(1);

  });

  it("should allow additional data within broadcast", function() {

    var fn = jasmine.createSpy('fn'),

    o =  {prop:11};

    mBus.addEventListener("myEvent", fn);

    expect(mBus.length("myEvent")).toEqual(1);

    mBus.notify("myEvent", o);

    expect(fn.calls.length).toEqual(1);

    expect(fn).toHaveBeenCalledWith(o);

  });
})

As you can see in it is assume that apart from holding references to listeners message bus is stateless, which will makes some things down the line easier.

Let’s make these tests pass!


define(function() {

  var listeners = {};

  function addEventListener(event, fn){
    listeners[event] = listeners[event] || [];
    listeners[event].push(fn);
  }

  function removeEventListener(event, fn) {
    if (listeners[event] === undefined) {
      // do nothing
    }
    else if (listeners[event].length === 1) {
      listeners[event] = []
    } else {
      var id = listeners[event].indexOf(fn);
      if (id !== -1) {
        listeners[event].splice(id, 1);
      }
    }
  }

  function removeAllListeners(event) {
    if (!event) return;
      listeners[event] = [];
  }

  function notify(event, data) {
    if (listeners[event] === undefined) {
      // skip
    } else if (listeners[event].length) {
      listeners[event].forEach(function(fn) {
        fn(data);
      }
    }
  }

  function length(event) {
    return listeners[event] && listeners[event].length || 0;
  }

  function clear() {
    listeners = {};
  }

  return {
    addEventListener: addEventListener,
    removeEventListener: removeEventListener,
    removeAllListeners: removeAllListeners,
    notify: notify,
    clear: clear,
    length: length
  }

});

Rewrite components

So now we have very basic implementation of MessageBus, which can be applied to solve our problems with cyclic dependencies. We’re ready to rewrite our components, in a way that makes makes them independent of each other.


define(["MessageBus"], function(mBus) {

  mBus.addEventListener("dao:init", dbSettingsReady);

  var settings = readFsSettings();
  settingsReady(settings);

  function readFsSettings(){
	return {};
  }

  function settingReady(key, value) {
    mBus.notify("settingReady:" + key, value);
  }

  function dbSettingsReady(dbSettings) {
   settingsReady(dbSettings);

   settings.__proto__ = dbSettings;
  }

  function settingsReady(s) {
    for(var i in s) {
      settingReady(i, s[i]);
     }
  }

// no direct access to settings!

});
describe(["MessageBus"], function(mBus) {
 var dao = new Dao();
 mBus.addEventListener("settingReady:url", function(url) {
    dao.init(url);
    mBus.notify("dao:init");
 });

 return dao;
});

Testing

As you can see some of the logic is hidden behind events and there’s no way to access them directly, so how this makes our test cases easier?

spyies to the rescue!

Using jasminejs spy method we can pretend there’s an instance of mbus and inovke our logic as if there are real interactions.


describe("Settings", function() {

  // prepare mock dependencies
  var mBusMock = jasmine.createSpyObj("mBus", ["addEventListener"]);

  define("mBusMock", function() {
    return mBusMock;
  });

  var r = require.config({
    map: {
      'Settings' : {
        'MessageBus': 'mBusMock',
        'Dao': 'daoMock'
      }
    }
  });

  define(["Settings"], function(s) {
    describe("Settings", function() {
      it("should register 1 event listeners", function() {
        expect(mBusMock.addEventListener.calls.length).toEqual(1);
      });
      it("should register for dao:init event", function() {
        expect(mBusMock.addEventListener).toHaveBeenCalledWith("dao:init", jasmine.any(Function));
      });
      it("should read dao settings", function() {
        mBusMock.addEventListener.calls[0].args[1].call();
      });
    });
  });
});

Pros

No cyclic dependencies

As you can see now modules have no cyclic dependencies so we solved our main issue.

Encapsulation

If you look closer at settings modules you’ll notice, that currently there’s no way to acces its state from outside world, which helps us hide information from outside world.

Testing made easier

As you can see in last requirement for settings – by using jasmnejs spies, we can access registered listener and call it on our own, which helps allows us to test encapsulated logic and does not for module author to break its contract only for the sake of proper testing.

Cons

Application flow

When first entering world of indirect events you might be intimidated and loose control over what happens where – fortunately this is easy to tackle and after relatively small amount of time, when your way of thining adjusts to having „man-in-the-middle” you’ll discover a completely new world ahead of you – it really is worth to try!

Soft dependencies

As mentioned earlier there are no direct dependncies between modules, so static code analysis is much harder if possible at all but in case of such a dynamic language as JavaScript this should not be an issue – just prepare yourselves for it by introducing proper conventions and follow it consistently.

Exception handling

Since you have no direct access to your callee, you can find it difficult to eliminate bugs from your software. Just prepare yourself for situations where your subscribers throw an error and handle it consistently, e.g. by logging it.

You might also try to post another error event but beware not to create a deadlock.

Event driven lifecycle

This is somewhat similar to point #1 – in the beginning you can find it hard to properly define your modules/application lifecycle, because each of them will receive different event. After a while you’ll find out that grouping events and hiding them behind message bus makes your life much, much easier.

Summary

During this article I’ve gone through possible headaches caused by cyclic-dependencies with requirejs and pains it might cause.

Next thing was to decompose modules so they’re totally clueless of one another and communicate only through message bus.

Than I showed how to tackle testing with jasminejs spies and how testing was made much easier through real encapsulation of logic inside a module.

Useful links