Using jQuery Deferred To Create Compound Objects From Multiple Asynchronous Data Sources
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.
Want to use code from this post? Check out the license.
Reader 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!
@Randall,
Thank you for making my day, good sir! :D
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!
Can't post long comments? https://gist.github.com/jamesmortensen/294457adb62cc18bf7a2 Thanks! Hope this helps.
Ok, so now I'll try a bit of context since the comment form wouldn't tell me what was wrong with the post. (It apparently didn't like me putting the Gist in the website field of the comment form).
Thanks for putting together all your demos! I wanted to see this one with actual, real JSONP requests from the server, so I used the echotest website and a locally-hosted JSON file to create a notes array and a contact object. It runs using the Node.js http-server so it's really easy to get setup. Just run:
$ npm init # get dependencies
$ node_modules/http-server/bin/http-server -p 8091 # run local server for the array.json notes.
Hope this helps and thanks again!