Friday, March 30, 2012

Making HipsterModel Even Easier

‹prev | My Chain | next›

Up tonight, I document and, if necessary, polish off HispterModel, the model class in my Dart Hipster MVC library.

I see that I am optionally allowing the collection to be set on the model (it's useful for delegating the backend URL):
class HipsterModel implements Hashable {
  // ...
  HipsterModel(this.attributes, [this.collection]) { /* ... */ }
}
This was a bit of premature optimization on my part as I am not using the option collection parameter anywhere. Even when the collection creates a model, it assigns itself after the model is built:
class HipsterCollection implements Collection {
  // ...
  _buildModel(attrs) {
    var new_model = modelMaker(attrs);
    new_model.collection = this;
    return new_model;
  }
}
If this ever becomes useful, I can add it back or define a named constructor (e.g. new ComicBook.withCollection(comic_collection)). For now, I remove it.

This leads me to wonder again about the model subclass. Currently, I have to define a redirection to super() in all subclasses:
class ComicBook extends HipsterModel {
  ComicBook(attributes) : super(attributes);
}
I would prefer to only define that as necessary. In other words, I would like to reduce the minimal HipsterModel subclass to:
class ComicBook extends HipsterModel {}
In this case, Dart has an implied redirection constructor that effectively calls the superclass constructor with no arguments. In other words, the above is the same as defining the subclass as:
class ComicBook extends HipsterModel {
  ComicBook(): super();
}
As defined now, HispterModel will not support this because I still require attributes. This will work if I make attributes optional:
class HipsterModel implements Hashable {
  /// The internal representation of the record.
  Map attributes;

  HipsterModel([this.attributes]) {
    on = new ModelEvents();
    if (attributes == null) attributes = {};
  }
  // ...
}
I still have the option to define a constructor on ComicBook that takes an argument, but now I do not have to do so. Since I do not, my modelMaker() factory function can no longer pass arguments to the constructor:
class Comics extends HipsterCollection {
  get url() => '/comics';
  modelMaker(_) => new ComicBook();
}
That modelMaker() function still needs to accept an argument in case another subclass wants to use it. Here, I use the convention of an underscore parameter to signify that it is being discarded.

One last hoop that I have to go through is for the collection to assign attributes when they are discarded like that. A simple isEmpty() suffices:
  _buildModel(attrs) {
    var new_model = modelMaker(attrs);
    // Give the factory a chance to define attributes on the model, if it does
    // not, explicitly set them.
    if (new_model.attributes.isEmpty()) new_model.attributes = attrs;
    new_model.collection = this;
    return new_model;
  }
With that, my empty model definition now works, as evidenced by my sample comic book application still working:


That might seem like a lot of work to go through just to eliminate one line, but I am eliminating one line that users of my library need to define. Forcing potential users to write extra code is never cool.

So... win!

Lastly, I notice that I have a TODO in HipsterModel#save():
  // TODO: update
Back before I switched the data sync layer to HipsterSync, this must have seemed to me a difficult task. Now, all that I need to do is pass in 'create' or 'update' depending on whether or not the model has been previously saved to the backend:
  Future save() {
    Completer completer = new Completer();
    String operation = isSaved() ? 'update' : 'create';
    Future after_call = HipsterSync.call(operation, this);
    // ...
  }
With that, I have a well-documented, fully operational Death S... er, model class in Hipster MVC.


Day #341

No comments:

Post a Comment