Friday, March 2, 2012

From the Future: Hipster MVC

‹prev | My Chain | next›

Yesterday, I failed miserably to get Dart to work with IndexedDB in Dartium. It was a pretty disappointing outing all around, but not a complete waste. I eventually came across a nice example of Dart IndexedDB written by Seth Ladd. It only works when compiled into Javascript, so it was not directly useful to me. But it did get me to thinking about my callback ways.

Coming from a very functional Javascript / node.js background, I am perfectly comfortable slinging callbacks about. In my Hipster MVC library, for example, I supply a callback when I save individual models:
  create(attrs) {
    var new_model = modelMaker(attrs);
    new_model.collection = this;
    new_model.save(callback:(event) {
      this.add(new_model);
    });
  }
When I read through Seth's IndexedDB code, it struck me how much he relies on Futures. This, naturally enough, made me feel all inadequate in my Dart coding. Well today, I shall be inadequate no more! Well... at least in this area.

Looking into HipsterModel, I see that my save is awash in callback... stuff:
class HipsterModel {
  // ...
  save([callback]) {
    HipsterSync.call('post', this, options: {
      'onLoad': (attrs) {
        attributes = attrs;

        var event = new ModelEvent('save', this);
        on.load.dispatch(event);
        if (callback != null) callback(event);
      }
    });
  }
  // ...
}
Instead of this approach, I declare that save() returns a Future. In the method body, I need a Completer, whose future I can return:

  Future<HipsterModel> save() {
    Completer completer = new Completer();

    HipsterSync.call('post', this, options: {
      'onLoad': (attrs) {
        attributes = attrs;

        var event = new ModelEvent('save', this);
        on.load.dispatch(event);

        completer.complete(this);
      }
    });

    return completer.future;
  }
The future is not invoked until the Completer completes, which happens inside the onLoad callback (say, I can probably get rid of that too). Back in the Collection class, I now need a then() method for the returned Future:
class HipsterModel {
  // ...
  create(attrs) {
    var new_model = modelMaker(attrs);
    new_model.collection = this;
    new_model.
      save().
      then((saved_model) {
        this.add(new_model);
      });
  }
  // ...
}
Ooh! I like that. It reads a heck of a lot better than my callback parameter. Here, I can read that I save my new model, then add it to the current collection.

A quick sanity check verifies everything still works:


Back in HipsterModel#save, I am still invoking HipsterSync.call() with a callback:
class HipsterModel {
  // ...
  Future<HipsterModel> save() {
    Completer completer = new Completer();

    HipsterSync.call('post', this, options: {
      'onLoad': (attrs) {
        attributes = attrs;

        var event = new ModelEvent('save', this);
        on.load.dispatch(event);

        completer.complete(this);
      }
    });

    return completer.future;
  }
  // ...
}
If I replace the callback in HipsterSync with a Future, the callback becomes:
class HipsterModel {
  // ...
  Future<HipsterModel> save() {
    Completer completer = new Completer();

    HipsterSync.
      call('post', this).
      then((attrs) {
        this.attributes = attrs;
        on.load.dispatch(new ModelEvent('save', this));
        completer.complete(this);
      });

    return completer.future;
  }
  // ...
}
That is so much nicer to read. I am calling HipsterSync, telling it to POST the model. Then, I take the attributes returned from the sync and assign them to the model's attributes, dispatch any events, and, finally, complete the save() completer from earlier.

That is much cleaner than the callback approach. I may have to start using custom-built Futures in Javascript. One thing missing from this is exceptions—I have a lot of happy path going on. Fortunately, there are exception facilities built into Futures. I will take a look at those tomorrow.


Day #313

No comments:

Post a Comment