Using jQuery Deferred To Create Compound Objects From Multiple Asynchronous Data Sources

Posted January 11, 2012 at 11:02 AM by Ben Nadel

Tags: Javascript / DHTML

As of now, all of my experimentation with modular JavaScript application architecture has been exclusive to client-side code. That is, it involves Views and Controllers, but no real sense of any Model that is tied to a persistent data store. As I've started to think about experimenting with the Model facet of client-side MVC (Model-View-Controller), I've run into some interesting concerns. Specifically, creating client-side domain entities that compose data from multiple remote data source (ie. API resources). As I've been mulling this problem over in my mind, it occurred to me that this might be an awesome place to use the jQuery.when() method from the jQuery Deferred library.


 
 
 

 
  
 
 
 

To experiment with this, I wanted to create a simplified scenario in which I could have a "Contact" entity that contains both contact data and "Note" data. The contact information would come from a theoretical remote resource for contacts while the note information would come from a theoretical remote resource for notes.

To abstract the layers of data access, I've created two Gateway classes that would be used to access the remote APIs:

  • NoteGateway()
  • ContactGateway()

Building on top of these gateways, I then created two Service classes that would be used to construct and return client-side domain entities:

  • NoteService()
  • ContactService()

For this exploration, I'm not really creating any actual domain entities; both of our service layer objects simply return native Array and Object instances that contain the relevant information.

Since the gateways are retrieving data over subsequent HTTP requests, the data is going to come back asynchronously. This means that the gateways, as well as everything that depends on them, will have to deal with promise objects. Since data can't be returned directly, the promise of data will have to be resolved or rejected in time.

Let's take a look at our Gateway objects. As you read through the code below, you'll see that we're not actually making any HTTP requests. However, in order to mimic the asynchronous nature of HTTP requests, we are creating and returning jQuery Deferred promises.

note-gateway.js - Our Note Gateway For Remote API Access

  • // Define the Note gateway.
  • window.NoteGateway = (function( $ ){
  •  
  •  
  • // I return an initialized component.
  • function NoteGateway(){
  •  
  • // Return the initialize object.
  • return( this );
  •  
  • }
  •  
  •  
  • // Define the class methods.
  • NoteGateway.prototype = {
  •  
  •  
  • // I get the note data for the given contact ID.
  • getNotesByContactId: function( id ){
  •  
  • // Since the data access for notes is (eventually)
  • // going to be asynchronous, we have to define a
  • // promise to hold the result.
  • var result = $.Deferred(
  • function( deferred ){
  •  
  • // For demo pursposes, we are just going to
  • // return a static set of notes. We'll need
  • // to resolve the deferred result with this
  • // collection.
  • deferred.resolve(
  • [
  • "Her birthday is on the 24th.",
  • "She likes puppies more than kittens.",
  • "Favorite number is 24"
  • ]
  • );
  •  
  • }
  • );
  •  
  • // Return the promise of a result.
  • return( result.promise() );
  •  
  • }
  •  
  •  
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Return the gateway constructor.
  • return( NoteGateway );
  •  
  •  
  • })( jQuery );

As you can see, nothing much is going on here. Our NoteGateway class contains a single class method for accessing the notes for a given contact. The data is defined statically and is used to resolve the accessor promise.

contact-gateway.js - Our Contact Gateway For Remote API Access

  • // Define the Contact gateway.
  • window.ContactGateway = (function( $ ){
  •  
  •  
  • // I return an initialized component.
  • function ContactGateway(){
  •  
  • // Return the initialize object.
  • return( this );
  •  
  • }
  •  
  •  
  • // Define the class methods.
  • ContactGateway.prototype = {
  •  
  •  
  • // I get the contact data with the given ID.
  • getContactById: function( id ){
  •  
  • // Since the data access for contacts is (eventually)
  • // going to be asynchronous, we have to define a
  • // promise to hold the result.
  • var result = $.Deferred(
  • function( deferred ){
  •  
  • // For demo pursposes, we are just going to
  • // return a static set of contact data. We'll
  • // need to resolve the deferred promise.
  • deferred.resolve(
  • {
  • id: 2,
  • name: "Joanna Smith",
  • title: "Senior Web Developer",
  • age: 37
  • }
  • );
  •  
  • }
  • );
  •  
  • // Return the promise of a result.
  • return( result.promise() );
  •  
  • }
  •  
  •  
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Return the gateway constructor.
  • return( ContactGateway );
  •  
  •  
  • })( jQuery );

The ContactGateway class is pretty much identical to the NoteGateway; only, it returns a hash of contact data rather than an array of notes.

At this point, we have a way to access contact and note data stored on "remote" API resources. Now, we need to use these gateways to create our domain entities. In order to encapsulate the complexities of gateway interactions, we'll create a Service layer that acts a liaison between our Controller and Gateway classes.

Let's take a look at the NoteService class first since it is the easier one to understand. Notice that when we create our NoteService instance, we have to pass it a reference to our NoteGateway instance so that it knows how to access the remote data.

note-service.js - Our Note Service Layer

  • // Define the Note service.
  • window.NoteService = (function( $ ){
  •  
  •  
  • // I return an initialized component.
  • function NoteService( noteGateway ){
  •  
  • // Store the gateway.
  • this.noteGateway = noteGateway;
  •  
  • // Return the initialize object.
  • return( this );
  •  
  • }
  •  
  •  
  • // Define the class methods.
  • NoteService.prototype = {
  •  
  •  
  • // I get the notes for the given contact.
  • getNotesByContactId: function( id ){
  •  
  • // Since the data access for notes is retrieved
  • // asynchronously, we have to define a promise object
  • // to hold the result.
  • var result = $.Deferred();
  •  
  • // Get the data from the note gateway.
  • var dataCollection = $.when(
  • this.noteGateway.getNotesByContactId( id )
  • );
  •  
  • // When the data is retrieved, let's resolve the notes.
  • dataCollection.done(
  • function( noteData ){
  •  
  • // Resolve the notes promise - just pass through
  • // the raw array of string values.
  • result.resolve( noteData );
  •  
  • }
  • );
  •  
  • // Return the promise of a result.
  • return( result.promise() );
  •  
  • }
  •  
  •  
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Return the service constructor.
  • return( NoteService );
  •  
  •  
  • })( jQuery );

To keep this experiment simple, the NoteService instance has only one class method for accessing note data (for a given contact ID). And, since an asynchronous gateway is being used to access the persistant data, the note service layer must use and return deferred objects. Notice that in this scenario, I'm use the jQuery.when() method as a way to wait for the NoteGateway() result to be resolved. Since we only have one asynchronous method, I could have simply bound a "resolve" handler to the gateway; however, since I'm trying to experiment with multiple asynchronous data sources, I wanted to set up a pattern of using jQuery.when().

And, speaking of multiple asynchronous data sources, let's now take a look at the ContactService() class. This class assembles a contact "entity" using remote data from the contact and note APIs. In order to do this, the ContactService() instance will need references to the ContactGateway() as well as the NoteService().

contact-service.js - Our Contact Service Layer

  • // Define the Contact service.
  • window.ContactService = (function( $ ){
  •  
  •  
  • // I return an initialized component.
  • function ContactService( contactGateway, noteService ){
  •  
  • // Store the gateway.
  • this.contactGateway = contactGateway;
  •  
  • // Store the note service.
  • this.noteService = noteService;
  •  
  • // Return the initialize object.
  • return( this );
  •  
  • }
  •  
  •  
  • // Define the class methods.
  • ContactService.prototype = {
  •  
  •  
  • // I get the contact with the given ID.
  • getContactById: function( id ){
  •  
  • // Since the data access for contacts is retrieved
  • // asynchronously, we have to define a promise object
  • // to hold the result.
  • var result = $.Deferred();
  •  
  • // Get the contact data from the contact gateway and
  • // the note data from the note service.
  • var dataCollection = $.when(
  • this.contactGateway.getContactById( id ),
  • this.noteService.getNotesByContactId( id )
  • );
  •  
  • // When the data is retrieved, let's resolve the contact.
  • dataCollection.done(
  • function( contactData, notes ){
  •  
  • // For this demo, we'll create a simple Contact
  • // object rather than a true domain entity.
  • var contact = {
  • id: contactData.id,
  • name: contactData.name,
  • title: contactData.title,
  • age: contactData.age
  • };
  •  
  • // Now, append the notes that we got from the
  • // note service.
  • contact.notes = notes;
  •  
  • // Resolve the contact promise.
  • result.resolve( contact );
  •  
  • }
  • );
  •  
  • // Return the promise of a result.
  • return( result.promise() );
  •  
  • }
  •  
  •  
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Return the service constructor.
  • return( ContactService );
  •  
  •  
  • })( jQuery );

NOTE: The ContactService() takes a reference to the NoteService() rather than the NoteGateway() since the note data access is already being encapsulated completely by the NoteService().

The ContactService() has a single method for accessing a given contact. When this method is called, however, the service layer has to collect data from two remote resources - notes and contacts. This is where the jQuery.when() method starts to look a bit more useful:

  • // Get the contact data from the contact gateway and
  • // the note data from the note service.
  • var dataCollection = $.when(
  • this.contactGateway.getContactById( id ),
  • this.noteService.getNotesByContactId( id )
  • );

Since both the note access and the contact access happen asynchronously, the jQuery.when() method allows us to easily wait until both remote resources have been resolved locally. And, once they have, we can then assemble the local contact "entity" using both the retrieved contact and note data.

Now that we've taken a look at all the layers of the application that deal with data access and synthesis, let's examine the demo that actually pulls it all together:

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Creating Compound Objects From Multiple Asynchronous Data Sources</title>
  •  
  • <!-- Load and run demo scripts. -->
  • <script type="text/javascript" src="./jquery-1.7.1.min.js"></script>
  • <script type="text/javascript" src="./note-gateway.js"></script>
  • <script type="text/javascript" src="./note-service.js"></script>
  • <script type="text/javascript" src="./contact-gateway.js"></script>
  • <script type="text/javascript" src="./contact-service.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Create Note access classes.
  • var noteGateway = new NoteGateway();
  • var noteService = new NoteService( noteGateway );
  •  
  • // Create Contact access classes.
  • var contactGateway = new ContactGateway();
  • var contactService = new ContactService( contactGateway, noteService );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Get the contact with the given ID. Since this involves
  • // asynchronous data retrieval, we'll get a promise object
  • // as a response.
  • var contactResult = contactService.getContactById( 2 );
  •  
  • // When the contact comes back, log it.
  • contactResult.done(
  • function( contact ){
  •  
  • console.log( contact );
  •  
  • }
  • );
  •  
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

As you can see, we first create instances of our gateway and service classes. Then, we use our ContactService() instance to get the given contact. When the resultant promise is resolved, we log the contact to the console:

age: 37
id: 2
name: "Joanna Smith"
notes: [ "Her birthday is on the 24th.", .... ]
title: "Senior Web Developer"

As you can see, our contact "entity" composes both the remote contact information and the remote note information.

I don't know too much about Object-Oriented Programming (OOP); and, I hardly know anything about modular JavaScript application architecture; but, it seems likely that at some point I'll have to assemble local data using the composite of multiple remote data sources. In such a case, the jQuery Deferred class and methods really just seem to shine. The more I use jQuery Deferred objects, the more I find reasons to use them. They simply rock, hardcore style.




Reader Comments

Jan 11, 2012 at 12:33 PM // reply »
158 Comments

Unfortunately I have nothing to add, but I thought I'd make your day anyway (and subscribe to future comments).

Enjoy reading the JQuery posts!


Jan 11, 2012 at 1:18 PM // reply »
11,238 Comments

@Randall,

Thank you for making my day, good sir! :D


Mar 29, 2012 at 4:22 PM // reply »
1 Comments

Thanks for the excellent tutorial! I have spent ages trying to get my head around jQuery Deferred objects, and whilst there are many tutorials around none of them have explained it as clear and concisely as you have.

Most importantly, you showed in your code how to pass back the data you received from your external data sources - which no other tutorial I have come across has done.

Thanks again, you have been a real time saver for me!


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 19, 2013 at 2:31 PM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
It's funny really just how well that image describes the way I would imagine most people that go with angular for some project is. I have had a similar roller-coaster ride with it as well, but not qu ... read »
May 17, 2013 at 7:42 PM
HashKeyCopier - An AngularJS Utility Class For Merging Cached And Live Data
Ben - thanks so much for posting these Angular articles and findings, they've been a huge help towards learning one of the more 'complex' JavaScript frameworks out there (IMO). I have been using Angu ... read »
May 16, 2013 at 5:01 PM
UPDATE: Parsing CSV Data Files In ColdFusion With csvToArray()
Your code was the closest thing I've found to obtaining some direction for converting ISO fields to values that CF can translate properly. Thank you for posting! ... read »
May 15, 2013 at 10:37 PM
Very Simple Pusher And ColdFusion Powered Chat
hi id making plz easy ... read »
May 15, 2013 at 6:07 PM
Making SOAP Web Service Requests With ColdFusion And CFHTTP
Ben, you once again saved my bacon at work. Thank you, thank you, thank you! ... read »
May 15, 2013 at 4:15 PM
What If All User Interface (UI) Data Came In Reports?
@Josh, Thanks! @Ben, I definitely recommend the David West book "Object Thinking" I've been quoting from. It goes deeply into the philosophy and history of OO programming. His breadth ... read »
May 15, 2013 at 11:36 AM
Ask Ben: Print Part Of A Web Page With jQuery
I found this helpfull when you need to keep (refresh) the original parent page after closing the iframe child print dialog (Hoping you're not using a form at this time so it won't submit again): On ... read »
May 14, 2013 at 7:13 PM
What If All User Interface (UI) Data Came In Reports?
@Jonah, If there's any books you'd recommend on the subject of domain modelling, I'd love to hear it. I just downloaded the free PDF of "Domain Driven Design Quickly". Figured I'd give it ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools