Question

I am designing a javascript API which wrap's my REST API. I generally want to avoid lots of verbose and confusing nested callbacks and have been reading up of the Deferred jQuery goodness.

Let's imagine my library 'myLib' which represents people objects and ways to traverse between people objects. It has a bunch of method's like 'dad', 'boss', 'assistant' etc which need to do an ajax request to find some data and return another related 'people' object. But I want them to return a deferred object which also has myLib's methods which I can chain together, to write really terse simple code like this:


 myLib('me').dad().boss().assistant(function(obj){
   alert(obj.phone); // My dad's, bosses assistants phone number
 }, function(){
   alert('No such luck);
 });

This creates a 'me' person object, then does the first ajax call to find my details, then uses that data to do another call to find out my parent, then again to find my boss, then another to get the assistant, and then finally that is passed to my callback and I handle it. Kinda like jQuery's chained traversing methods but asynchronous.

Passing a function in at any point, but usually the last method, would internally get called when the last Deferred object in the chain is resolved. The second function is the failure callback and is called if any of the deferred objects in the chain is rejected.

I'm thinking I need to create a jQuery deferred object and then extend it but not sure if thats the "best" way.

So what is the best practice way to achieve my minimalist API goal? Basically I want all the method names to be 100% in the domain problem name space and not polluted with lots of 'when', 'done', 'success' etc.

And are there examples of similar clean API's I can emulate somewhere?

Was it helpful?

Solution 3

I've got this working perfectly with an internal promise, so I immediately create a Person objects with no data. All it contains is a promise for data later. Method's like parent() create a new promise which chain's off the current promise. There is probably a way to make this simpler using pipe() but haven't quite figured it out yet.


myLib = {
  find: function(id){
    var person = new Person();
    person.promise = $.ajax(.....);
  }
};

function Person(){
}
Person.prototype.parent(){
  var person = this;
  var parent = new Person();
  var deferred = $.Deferred();
  this.promise.then(function(data){
    var promise = $.ajax({url: '/person/'+data.parent+'/json'});
    promise.done(function(obj){
      person.data = obj.data;
      deferred.resolve(node);
    });
  }, function(error){
    deferred.fail();
  });
  parent.promise = deferred.promise();
  return parent;
}

Person.prototype.get(callback){
  this.promise.then(function(data){
    this.data = data;
    callback(this);
  });
}


Usage/Test:

myLib.find('12345').get(callback);
myLib.find('12345').parent().get(callback);



OTHER TIPS

I'm going to leave my Person implementation alone, as I think it mostly fulfills its purpose:

function Person(o) {
  this.id = o.id;
  this.name = o.name;
}

Person.prototype.dad = function(done, fail) {
  var promise = $.getJSON('/people/' + this.id + '/dad').pipe(Person, null);
  promise.then(done, fail);
  return new Person.Chain(promise);  
};

Person.prototype.boss = function(done, fail) {
  var promise = $.getJSON('/people/' + this.id + '/boss').pipe(Person, null);
  promise.then(done, fail);
  return new Person.Chain(promise);  
};

For the Person.Chain implementation, we have two problems: every time you call a getter method, it really should return a new Person.Chain, and that new Person.Chain should be "nested": it needs to chain the results of the AJAX calls together. This should solve both problems.

This approach takes a few lines of glue, so first let's make sure we don't have to duplicate it over and over:

Person.Chain = function(promise) {
  this.promise = promise;
};

Person.Chain.prototype.assistant = function(done, fail) {
  return this.pipe('assistant', done, fail);
};

Person.Chain.prototype.dad = function(done, fail) {
  return this.pipe('dad', done, fail);
};

Person.Chain.prototype.boss = function(done, fail) {
  return this.pipe('boss', done, fail);
};

We just need to define as many of these wrapper methods as there are getter methods on Person. Now, to implement pipe:

Person.Chain.prototype.pipe = function(f, done, fail) {
  var defer = new $.Deferred();
  defer.then(done, fail);

  this.promise.pipe(function(person) {
    person[f](function(person) {
      defer.resolve(person);
    }, function() {
      defer.reject();
    });
  }, function() {
    defer.reject();
  });

  return new Person.Chain(defer.promise());
}

First, we explicitly create a new Deferred object, and attach the done and fail handlers (if any) to it. Then we attach a function that will call whatever f was passed (dad, assistant, boss, etc.) on the Person that will be returned from the previous function. Finally, when that function resolves, we explicitly resolve the Deferred object we created. Now we can chain together successive calls like this:

jake = new Person({id: 3, name: 'Jake'});

jake.dad().boss().assistant(function(person) {
  alert("Jake's dad's boss's assistant is " + person.name);
});

Note that failure handling is kind of verbose, but we need that so that if you chain a bunch of calls together, an early failure will still pass it's reject() calls down the line all the way to a failure callback given at the end.

It's also totally legal to do this:

jake.dad(function(person) {
  alert('Dad is ' + person.name);
}, function() {
  alert('Dad call failed');
}).boss(function(person) {
  alert('Jake dad boss is ' + person.name);
}, function() {
  alert('One of the calls failed');
});

If the first call fails, both failure callbacks will be called in order. If only the last one fails, only that one will be called.

Big caveat, none of this code is tested. But, theoretically, it's a working approach.

I think what you want is a query builder, where the methods that add criteria (like "dad" and "assistant" are all chainable. Also, you want it so that at any point you can pass a callback, and this means execute the query.

So I would do it like this:

function PersonQuery(personName) {
  this.criteria = [];
  criteria.push({name:personName});
}

PersonQuery.prototype.dad = function(doneCallback) {
    this.criteria.push({relationship:"dad"});
    _execute(doneCallback);
    return this;
}

PersonQuery.prototype.boss = function(doneCallback) {
    this.criteria.push({relationship:"boss"});
    _execute(doneCallback);
    return this;
}

PersonQuery.prototype.assistant = function(doneCallback) {
    this.criteria.push({relationship:"assistant"});
    _execute(doneCallback);
    return this;
}

PersonQuery.prototype._execute = function(doneCallback) {
    if (!doneCallback) {
       return;
    }
    $.ajax({"data": this.criteria}).success(function(responseData) {
       doneCallback(responseData);   
    });
}

Then, to use this, your example would become:

   new PersonQuery("me").dad().boss().assistant(function(obj) { 
    alert(obj.phone); });
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top