Partager via


Chapter 8: Communication

Introduction | Direct Method Invocation - Getting a Widget Reference, Method Invocation in Mileage Stats, A Helper for Method Invocation | Raising and Handling Events | Publishing and Subscribing - Pub/Sub in Mileage Stats, The pubsub Object, An Example of Subscribing and Publishing, Finding the Right Balance | Summary | Further Reading

Introduction

In a client-side application composed of discrete objects, many of those objects need to communicate with one another. You have a few options when deciding how they do this. Generally, your options are direct method invocation, and an indirect communication mechanism such as events. These options give you the ability to control the amount of complexity applied to the object's interface and implementation, and allow you to control the level of coupling the objects have with each other. For example, an object that invokes a method on another object must have a direct reference to that object. If you chose to implement a form of events, the objects may either have a direct dependency or use a broker object to enable them to communicate in a loosely coupled manner. This broker prevents the objects from needing to have direct knowledge of each other.

Comparison between direct and loosely coupled communication

Hh404091.3bd00693-24a6-4bfe-a1a2-07c38c417065(en-us,PandP.10).png

Most applications will use more than one form of communication between their objects because there are advantages and disadvantages to each option. Your responsibility is to apply an appropriate balance based on the application.

In this chapter you will learn:

  • How to implement direct method invocation between widgets.
  • The advantages and disadvantages of direct method invocation.
  • How to implement event triggering and handling between widgets.
  • How to control dependencies between the objects triggering events and those handling them.
  • When using the Publish/Subscribe (pub/sub) pattern is appropriate.
  • How to use pub/sub to manage multiple handlers and reduce coupling.
  • How the Mileage Stats Reference Implementation (Mileage Stats) implements these options and how the team chose various communication options.

Direct Method Invocation

When a widget needs to communicate with another widget and no other objects are interested in the communication, the calling widget can use direct method invocation. This option is the easiest to understand, the easiest to implement, and the easiest to follow when tracing code paths. You could use this option for all communication, but if your application has many objects and many calls between those objects, this approach will force the objects to have more methods on their interface than the loosely-coupled options require and it can greatly increase the coupling between those objects.

Getting a Widget Reference

Many of the JavaScript objects in Mileage Stats are implemented as jQuery UI Widgets, which have a specific way to expose public methods. After a widget has exposed a method, the calling widget needs a reference to it before it can invoke the method. One option would be for the calling widget to know how to get a reference to the widget it needs to call. The widget pseudo selector is a good way to do this without having to know the exact elements the widget is attached to. This approach to getting a widget reference requires the calling widget to know that it is invoking the method on a specific widget by name. Another option is to have the references passed in as part of the options object. This allows the calling widget to invoke the method without having to know the name of the widget it is calling. For information on how to expose and invoke methods on widgets, see "Public Methods" in Chapter 3, "jQuery UI Widgets." For more information on the widget pseudo selector see "Using the Pseudo Selector" in Chapter 3, "jQuery UI Widgets."

Method Invocation in Mileage Stats

In the following example, an object reference called charts is captured when the charts widget is created. This reference is then passed as the value to the charts option when initializing the layoutManager widget.

// Contained in mileagestats.js
charts = $('#main-chart').charts({
    sendRequest: mstats.dataManager.sendRequest,
    invalidateData: mstats.dataManager.resetData
});
    
$('body').layoutManager({
    subscribe: mstats.pubsub.subscribe,
    pinnedSite: mstats.pinnedSite,
    charts: charts,
    header: header,
    infoPane: infoPane,
    summaryPane: summaryPane,
    vehicleList: vehicleList
});

The layout manager uses this reference to invoke methods on the charts widget. For example, when the layout manager is coordinating the transition to the charts layout, it calls the chart's moveOnScreenFromRight method.

// Contained in mstats.layout-manager.js
_goToChartsLayout: function() {
    this._summaryPane('moveOffScreen');
    this._vehicleList('moveOffScreen');
    this._infoPane('moveOffScreenToRight');
    this._charts('moveOnScreenFromRight');
},

You probably noticed that the method calls in _goToChartsLayout are not using the calling convention described in the see "Public Methods" section in Chapter 3, "jQuery UI Widgets."

A Helper for Method Invocation

Rather than using this.options.widgetName.widgetName('methodName'), many of the calls in Mileage Stats, and particularly in the previous code example, have been simplified to this._widgetName('methodName'). Mileage Stats uses a helper for method invocation to improve readability. This isn't necessary; the long form works fine, but the helper method does take advantage of the dynamic nature of JavaScript and can make the code more readable and less repetitive. The setupWidgetInvocationMethods function dynamically creates an underscore-prefixed method on the host argument for each widget whose name is passed in the widgetNames argument. The options argument tells the helper function where to find the implementation of the widget functions.

// Contained in mstats.utils.js
function buildFunction(widget, options) {
    var context = options[widget],
        fn;
        
    if(!context) {
        mstats.log('Attempted to create a helper for ' + 
            widget + ' but the widget was not found in the options.');
        return;
    }
        
    fn = context[widget];
    return function() {
        var result = fn.apply(context, arguments);
        return result;
    };
}

mstats.setupWidgetInvocationMethods = function(host, options, widgetNames) {
    var i,
        widgetName;

    for (i = widgetNames.length - 1; i >= 0; i -= 1) {
        widgetName = widgetNames[i];
        host["_" + widgetName] = buildFunction(widgetName, options);
    }
};

The setupWidgetInvocationMethods function is called when the layout manager is first created. This function call dynamically creates new methods called _charts, _header, _infoPane, _summaryPane, and _vehicleList that will make invoking methods on those widgets much easier, as you can see in the _goToChartsLayout method above.

// Contained in mstats.layout-manager.js
_create: function() {
    ...
    // add on helper methods for invoking public methods on widgets
    mstats.setupWidgetInvocationMethods(this, this.options, 
        ['charts', 'header', 'infoPane', 'summaryPane', 'vehicleList']);
    ...
},

In addition to the layout manager, the above helper method is also used in the _create method of the info pane, summary, and vehicle details widgets.

Direct method invocation is useful when an object must invoke a method on another object. However, depending on the direction of the method call, it may result in incorrect dependencies. For example, in the case of a widget that manages other widgets in a parent/child relationship, the child widgets should not have knowledge of their parent. This is where events can be helpful.

Raising and Handling Events

Events allow an object to communicate with other objects without a direct reference to them. Rather than having to know how to resolve the reference to the receiver or how to invoke a specific method, event handlers only need to know the name of the event and any data sent with it. Therefore, using an event is more appropriate than invoking a method when a child widget needs to communicate with its parent. It's acceptable for a parent in a hierarchy to know about its children, but not the other way around. When a child widget knows too much about its parent, it makes it difficult or impossible to relocate the widget and use it from inside a different parent.

In Mileage Stats, the registration widget is created by and contained within the summary widget, which means that there is a parent/child relationship. When a user registers with the Mileage Stats application, the registration widget raises an event through its displayNameChanged event callback. The summary widget handles this event by setting the displayNameChanged option on the registration widget to the handler, which sets an option on the header widget.

// Contained in mstats.summary.js
_setupRegistrationWidget: function () {
    var that = this,
        elem = this.element.find('#registration');
            
    this.registration = elem.registration({
       dataUrl: elem.data('url'),
       publish: this.options.publish,
       sendRequest: this.options.sendRequest,
       displayNameChanged: function (event, args) {
           that._header('option', 'displayName', args.displayName);
       }
   });
}, 

When the user successfully registers, the registration widget raises the displayNameChanged event using the _trigger method. The _trigger method is available on the base jQuery UI Widget and accepts three arguments:

  • The name of the event to trigger.
  • The original event if it originated from a DOM event such as click.
  • The data to be sent to the event handler.
// Contained in mstats.registration.js (in the saveProfile method)
this.options.sendRequest({
    url: this.options.dataUrl,
    data: formData,
    cache: false,
    success: function () {
        that._startHidingWidget();
        that._showSavedMessage();
        // we update the username after successfully the updating the profile
        that._trigger('displayNameChanged', null, 
            { displayName: formData.DisplayName } );
    },
    error: function () {
        that._showSavingErrorMessage();
    }
});

Using events is a powerful way to reduce coupling without sacrificing communication between widgets. For more details on raising and handling events, see "Events" in Chapter 3, "jQuery UI Widgets."

Using options to expose the callback and wire up the handler reduces the coupling between the sender and receiver, but this requires that some other code, usually the code that creates the widgets, to perform the connections, as you can see in the _setupRegistrationWidget method above. An alternative to wiring up event handlers is to use the Publish/Subscribe pattern, which employs a broker to manage these connections.

Publishing and Subscribing

A common strategy for reducing the coupling between objects is for those objects to use an intermediary object, referred to as a broker, rather than communicating directly with each of their dependencies. The Publish/Subscribe pattern (commonly referred to as pub/sub) applies this strategy. The pattern provides a way for objects to communicate without needing a reference to the source or the destination of the messages. Rather, the objects have references to the pub/sub implementation and know how to use it based on their needs.

In its most general form, the Publish/Subscribe pattern identifies publishers, topics, and subscribers. Publishers and subscribers are software components that need to communicate with each other. Topics represent the contract between the sender and receiver and are made up of a name and an optional message. The following figure illustrates the Publish/Subscribe pattern.

The Publish/Subscribe pattern

Hh404091.d2a366d8-150c-454c-91c8-1618d54c6dad(en-us,PandP.10).png

The pattern has two phases. The first phase is the subscription phase. Objects sign up to be notified when information about particular topics becomes available. For example, in the figure, Subscriber 2 is subscribed to messages about Topic A and Topic B, while Subscriber 1 is subscribed only to Topic A. The second phase is the publication phase, when publishers provide information about topics. For example, in the figure, the Publisher creates messages about Topic A and Topic B.

Each implementation of the Publish/Subscribe pattern provides the communications infrastructure that receives and delivers messages. However, the details of how messages are routed between publishers and subscribers can differ between implementations. For example, it is possible that messages can be delivered synchronously or asynchronously, depending on the situation.

Pub/Sub in Mileage Stats

Mileage Stats uses a pub/sub JavaScript object to broker communication between the objects and widgets that make up the client-side application. Global events are events that are not constrained to an isolated part of the application. These events may cause multiple widgets to update their data and UI, such as when a vehicle is deleted or a reminder is fulfilled. Publishers and subscribers do not communicate directly with each other. Instead, widgets subscribe using callback functions for relevant topics. The widgets managing UI elements publish topics in response to lower-level DOM events caused by user actions.

The pubsub Object

The Mileage Stats' pubsub interface provides publish, subscribe, and unsubscribe methods. Each method requires an event name. These events are defined in mstats.events.js as objects that behave like string constants. The following code shows the interface of the pubsub object.

// Contained in mstats.pubsub.jsmstats.pubsub = (function () {
    var queue = [],
        that = {};
    that.publish = function (eventName, data) { ... };   
    that.subscribe = function (eventName, callback, context) { ... };
    that.unsubscribe = function (eventName, callback, context) { ... };
    return that;
}(); 

The mechanism for creating the pubsub object uses an immediate function which executes as soon as its definition is parsed. As a result, the pubsub object is available simply by adding the mstats.pubsub.js file to the page.

The subscribe method stores event subscriptions, which are comprised of a callback in the form of a function and a context in the form of an object. More than one subscription is possible for a single event name. Here is the code.

// Contained in mstats.pubsub.js
/**
 * @param {string} eventName  The name of the event to publish
 *                            Uses a slash notation for consistency
 * @param {function} callback The function to be called when the 
 *                            event is published.
 * @param {object} context    The context to execute the callback
 */
that.subscribe = function (eventName, callback, context) {
    if (!queue[eventName]) {
        queue[eventName] = [];
    }
    queue[eventName].push({
        callback: callback,
        context: context
    });
};

The subscribe method modifies the queue variable. It adds a new array element for the name that is passed as the eventName argument if that element does not already exist. Then, it adds a new object to that array with the callback and context fields. These fields store the function to be invoked when the event is published, and the context that function should execute within.

The publish method invokes the registered callback for a given eventName. The publish method executes all callbacks that are associated with eventName by using the context that was provided in the subscription.

// Contained in mstats.pubsub.js
/**
 * @param {string} eventName  The name of the event to publish
 *                            Uses a slash notation for consistency
 * @param {object} data       Any data to be passed to the event
 *                            handler. Custom to the event.
 */
 that.publish = function (eventName, data) {
    var context, intervalId, idx = 0;
    if (queue[eventName]) {
        intervalId = setInterval(function () {
            if (queue[eventName][idx]) {
                context = queue[eventName][idx].context || this;
                queue[eventName][idx].callback.call(context, data);
                idx += 1;
            } else {
                clearInterval(intervalId);
            }
        }, 0);
    }
};

The code successively invokes each function that is stored in the queue array. It uses the setInterval function with a timeout of 0 milliseconds to execute the callbacks asynchronously. This allows each subscriber callback to be invoked before the previous callback finishes. This also means callbacks can be invoked before other callbacks have finished. A consequence of asynchronicity is that you cannot predict the order in which callbacks are invoked or finish.

In Mileage Stats, only the unit tests take advantage of the unsubscribe method. However, other applications may need to allow the dynamic subscription and unsubscription of events.

An Example of Subscribing and Publishing

When a user fulfills a reminder, several areas of the UI need to refresh their own lists of reminders. The application uses the mstats.events.vehicle.reminders.fulfilled event name (this is the topic) to communicate the fact that a reminder was fulfilled. This variable is initialized with the value "reminders/fulfilled" in the mstats.events.js file.

The Mileage Stats layout manager widget is a coordinating widget. It subscribes to the mstats.events.vehicle.reminders.fulfilled event when it is created. Whenever this event is published, the registered callback refreshes each of the layout manager's child widgets. The following code shows how the subscription is implemented, and what happens during the callback.

// Contained in mstats.layout-manager.js
_subscribeToGlobalEvents: function () {
    var state = {},
        that = this;
    ... 
   this.options.subscribe(mstats.events.vehicle.reminders.fulfilled,
       function() {
           // we need to refresh the summary reminders, fleet statistics, 
           // details pane, reminders pane, charts, and jump list
           that._summaryPane('requeryStatistics');
           that._summaryPane('requeryReminders');
           that._infoPane('requeryVehicleDetails');
           that._infoPane('requeryReminders');
           that._charts('requeryData');
           that.options.pinnedSite.requeryJumpList();
        }, this);
},

When the user fulfills a reminder, the reminders widget uses the pubsub object to publish the event. By passing the subscribe method into the layout manager as an option, the layout manager is able to participate in pub/sub without having to know how to resolve the dependency to the pubsub object.

// Contained in mstats.reminders.js
_fulfillReminder: function (fulfillmentUrl) {
    var that = this;

    this._showFulfillingMessage();

    this.options.sendRequest({
        url: fulfillmentUrl,
        dataType: 'json',
        cache: false,
        success: function () {
            that._showFulfilledMessage();
            that.options.publish(mstats.events.vehicle.reminders.fulfilled, {});
        },
        error: function () {
            that._showFulfillErrorMessage();
        }
    });
},

The options.publish method is initialized to be the value of mstats.pubsub.publish when the reminders widget is created. Even though the reminders widget does not have a direct connection to the layout manager, the layout manager receives notification of the reminders/fulfilled event because it has subscribed to that event. The reminders/fulfilled event does not require a data payload, so the second parameter to the publish call is an empty object.

Finding the Right Balance

While building Mileage Stats, the team initially wanted the widgets to be as loosely coupled as possible in hopes of gaining the most flexibility. To this end, pub/sub was used exclusively for communication between widgets. Because each widget had to manage all of its subscriptions and the topics it published, this approach reduced readability by making it difficult to follow code paths. It is reasonable to have some level of coupling between widgets, especially if it improves the readability of the code base. Reducing the number of pub/sub topics also simplified the implementation of the widgets.

After this experience, the team replaced some pub/sub messages with direct method calls and jQuery events. The following guidelines can be used to determine when you might choose one option over the other.

  • Use direct method invocation when only two objects are participating in the action, and when readability and simplicity are more important than coupling and flexibility.
  • Trigger an event when it is unacceptable for the sender and receiver to know more about each other than the message they are passing.
  • Use pub/sub when multiple subscribers are needed for a single message or the same message needs to be sent from multiple publishers to a single subscriber. Pub/sub is also a good choice when subscribers may be added and removed during the lifetime of the page.

Summary

When a client-side web application is composed of multiple, discrete objects, those objects have a number of communication options to choose from. To keep the solution flexible, pub/sub can be used to keep the objects from having to take dependencies on all the other objects they communicate with. Pub/sub implementations, such as the one in Mileage Stats, typically have a way to publish, subscribe, and unsubscribe. If only pub/sub is used, it can complicate the implementation of the objects that have to manage the many subscriptions and handlers. Alternatively, if only direct communication, such as method invocation, is used, it can complicate the interface of the object by requiring more members. Balancing these options allows the application to have flexibility while keeping the interfaces and implementations as simple as possible. Mileage Stats accomplishes this by using pub/sub for global messages that multiple objects must react to and uses method calls and events for everything else. Independent of the approach you take, you should understand the implications and balance them to achieve the simplicity and flexibility appropriate for your application.

Further Reading

For more information about the Publish/Subscribe pattern, see https://msdn.microsoft.com/en-us/library/ms978603.aspx.

For more information on how to use JavaScript to implement the Publish/Subscribe pattern, see "Understanding the Publish/Subscribe Pattern for Greater JavaScript Scalability" at https://msdn.microsoft.com/en-us/scriptjunkie/hh201955.aspx .

For more information about closure, see the Wikipedia entry: http://en.wikipedia.org/wiki/Closure_(computer_science).

For more information on how to expose and invoke methods on widgets, see "Public Methods" in Chapter 3, "jQuery UI Widgets."

For more information on the widget pseudo selector see "Using the Pseudo Selector" in Chapter 3, "jQuery UI Widgets."

For more information on raising and handling events, see "Events" in Chapter 3, "jQuery UI Widgets."

Next | Previous | Home | Community