HTML and CSS   XSLT   JavaScript   Images   Soft   Etc  
Dmitry Filatov

Low coupling in JavaScript. Custom events 31 October 2007


Task:

Improve flexibility of the code.

Each time I work on client functionality, I have to handle interrelated components. Meant by components are any objects: classes, widgets, controllers, etc. Straight-forward solution is to have component-to-component interaction strictly specified in the code. This may be effective when the connections are few, but even in such case the components will be overloaded with information about its neighborhood which is not actually supposed to be there. And when you get to the point of having many intercomponent relations, the code seems to turn into an unmanageable mess, and using it in another neighborhood is out of question.

Say, we’ve wrote a nice image gallery script enabling previews and animation. A click on the preview makes the full-size image appear slowly. All works fine—the gallery component is complete and independent. Then we decide that while the image is crawling out, the background should change. We add the code responsible for changing background into the gallery code. And there we have the first worry signal of excess coupling—the component begins to performs something that is not its concern. The next day it appears that some of the pages must use a different background and some must not, navigation must hide and registration form must close if it was opened. Should we start adding all this into the gallery code, it becomes totally dependent on its neighborhood which is, to make it worse, also variable. So the code turns into a jumble performing way too many redundant actions. Handling this thing is no longer possible.

What can be done here? Lets consider patterns. We’ll make use of two: Observer and Mediator. The terms are borrowed from GoF.

Observer

This pattern implies that there are two groups of participants: Observers and Observables. The former register to observe events that may be raised by the later; Observables notify Observers when a change occurs.

Here is the Observable interface suitable for the task:


  

attachObserver(sEventType, mObserver)
detachObserver(sEventType, mObserver)
notify(sEventType)

Where sEventType stands for the event and mObserver stands for the observer which can be a callback function as well as an object.

By the way, Javascript itself uses a similar model with callback function for adding event handlers to DOM elements.

Now each component using the Observable interface can recognize the events taking place. For example, the gallery could have the following events: onInit, onStartOpen, onEndOpen, onStartClose, onEndClose. When necessary, the component calls the Notify method providing information about the event, and all the registered observers receive notifications.

But how can it be that some components are to observe the events raised by other components without knowing anything about them?

Mediator

In order to maintain independence of the components, it’s better not to use the components themselves as observers (it’s another kind of information burden), but have mediators, objects that already know all about the current neighborhood, its components and interrelations. This technique allows us to change mediators depending on the neighborhood which frees the component code of redundant data and makes it possible to use it another time.

Example of Observer pattern implementation

The code involves arrays from the Common.js library.

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
function Observable() {

    this.aObservers = [];

}

Observable.prototype = {

    attachObserver : function(
        sEventType,
        mObserver
        ) {

        if(!(mObserver instanceof Object)) {
            return;
        }

        if(!this.aObservers[sEventType]) {
            this.aObservers[sEventType] = [];
        }

        this.aObservers[sEventType].push(mObserver);

    },

    detachObserver : function(
        sEventType,
        mObserver
        ) {

        if(this.aObservers[sEventType] && this.aObservers[sEventType].contains(mObserver)) {
            this.aObservers[sEventType].remove(mObserver);
        }

    },

    notify : function(
        sEventType
        ) {

        if(!this.aObservers[sEventType]) {
            return;
        }

        for(var i = 0, aObservers = this.aObservers[sEventType], iLength = aObservers.length; i < iLength; i++) {

            if(aObservers[i] instanceof Function) {
                aObservers[i](
                    sEventType,
                    this
                    );
            }
            else if(aObservers[i].update instanceof Function) {
                aObservers[i].update(
                    sEventType,
                    this
                    );
            }

        }

    }

};

To use this technique you should make Observable your base class.

If you involve Common.js, it looks like this:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
function MyComponent() {

    MyComponent.baseConstructor.call(this);

    ...

}

MyComponent.EVENT_TYPE_SUPER_EVENT_A = 'superEventA';
MyComponent.EVENT_TYPE_SUPER_EVENT_B = 'superEventB';

MyComponent.inheritFrom(
    Observable,
    {

        methodA : function() {

            ...

            /* If you need to notify the observers registered for
                MyComponent.EVENT_TYPE_SUPER_EVENT_A,
                call the Notify method */
            this.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_A);

            ...

        },

        methodB : function() {

            ...

            /* If you need to notify the observers registered for 
                MyComponent.EVENT_TYPE_SUPER_EVENT_B,
                call the Notify method */
            this.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_B);

            ...

        }


    }
    );

If inheritance fails for some reason, you can use delegation:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
function MyComponent() {

    ...

    this.oObservable = new Observable();

}

MyComponent.prototype = {

    attachObserver : function(
        sEventType,
        mObserver
        ) {

        this.oObservable.attachObserver(
            sEventType,
            mObserver
            );

    },

    detachObserver : function(
        sEventType,
        mObserver
        ) {

        this.oObservable.detachObserver(
            sEventType,
            mObserver
            );

    },

    methodA : function() {

        ...

        /* If you need to notify the observers registered for
            MyComponent.EVENT_TYPE_SUPER_EVENT_A,
            call the Notify method of object oObservable */
        this.oObservable.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_A);

        ...

    },

    methodB : function() {

        ...

        /* If you need to notify the observers registere for
            MyComponent.EVENT_TYPE_SUPER_EVENT_B,
            call the  Notify method of oObservable */
        this.oObservable.notify(MyComponent.EVENT_TYPE_SUPER_EVENT_B);

        ...

    }

};

And this is how you register an observer for the events raised by MyComponent:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
var oMyComponent = new MyComponent();

// The observer is callback function
oMyComponent.attachObserver(
    MyComponent.EVENT_TYPE_SUPER_EVENT_A,
    function(oMyComponent) {

        // The event has occured, the information is processed

    }
    );


// The observer is object
oMyComponent.attachObserver(
    MyComponent.EVENT_TYPE_SUPER_EVENT_B,
    oObjectOfSomeAnotherClass
    );

/* When event MyComponent.EVENT_TYPE_SUPER_EVENT_B occurs
    object oObjectOfSomeAnotherClass receives update notification */

Order a design...