Monday, February 13, 2012

Dunno Why I Avoid Strategy So

‹prev | My Chain | next›

I now have my Dart-based Hipster MVC framework able to query and update both local storage and remote data stores (via Ajax). But to do so, I have two different branches of my framework. Backbone.js has the nifty little Backbone.sync method, through which all queries and updates run. I hope to be able to replicate that in Dart.

I suspect that I will eventually succumb to typing various parts of this, but, for now, I will stick closer to Backbone's Javascript than idiomatic Dart. But immediately, I can think of a problem with this approach--it is not possible to redefine methods in Dart. By default, Backbone.sync() performs CRUD over Ajax. To change this behavior, one redefines Backbone.sync (i.e. not overriding it in a sub-class). Complicating matters is that both the model and collection classes need to have access to this overridden sync() method. This seems difficult, at best, if I cannot rewrite a single method.

My first attempt is to define a function that can be source'd into both the model and collection. For example, it would be nice if I could simply define a sync method:
// sync.dart
sync() {
  print("[sync]");
}
And then source() it in my collection sub-class:
#library('Collection class to describe my comic book collection');

#import('HipsterCollection.dart');
#import('Models.ComicBook.dart');

#source('sync.dart');

class Comics extends HipsterCollection {
  get url() => '/comics';
  modelMaker(attrs) => new ComicBook(attrs);
}
Back in my base class, I then try to replace my local storage version of fetch() with an attempt to invoke this sync() function:
class HipsterCollection implements Collection<HipsterModel> {
  // ...
  fetch() {
    try {
      sync();
    }
    catch (NoSuchMethodException e) {
      print("*** time to define a default sync");
    }
  }
  // ...
}
Unfortunately, this will not work—the NoSuchMethodException clause is always executed. It actually works as expected if I source() the sync.dart code directly into the HipsterCollection base class. This is because the source'd code is available to the enclosing library, but nowhere else (not even the base class of the enclosing library).

I need some way for the concrete class to communicate the strategy for syncing back in a way that the base class can manipulate it. So I am stuck defining a "syncer" (that really does not look right) class:
#library('Sync layer for the Hipster MVC');

class HipsterSync {
  sync() {
    print("[sync]");
  }
}
I can then specify this in my Comics concrete collection class:
class Comics extends HipsterCollection {
  get url() => '/comics';
  modelMaker(attrs) => new ComicBook(attrs);
  get syncer() => new HipsterSync();
}
And use it in the base class:
  fetch() {
    if (syncer != null) {
      syncer.sync();
    }
    else {
      // be ajax- like
    }
  }
That seems to work just fine. I do not see any other options besides supplying a syncing object. It is not as nice as Backbone.sync, with which I only need to define the sync strategy once. In my Dart MVC, I will have to supply the syncing strategy for each model and class in the application. That is less than ideal, but hopefully I can find ways to mitigate the pain.

Starting tomorrow.


Day #295

5 comments:

  1. What you would like to do is instead of defining sync as a top-level function, define it as a top-level variable with a function value. Then you could change the value of the variable.

    The only problem is that a top-level variable can only be initialized with a constant expression and functions aren't constant for some reason. The constant initializer rule might be relaxed, and maybe functions will be considered constant as well.

    So you can have the variable be null, and require an initialization function be called to set it to it's default value.

    ReplyDelete
    Replies
    1. Unless I am missing something, I cannot declare a top-level constant null and then set it later. Since it's a constant, it cannot be re-assigned, right?

      I thought about doing something similar with a Map. Currently in Dartium I cannot even get map literals defined as top-level constants (`sync = {};`). Even if I could, I have not figured out a way to see that top-level constant in a library, which is what needs to see it. Libraries seem to get their own variable space that does not include globals.

      Delete
    2. Top-level variables don't need to be constant. If they're not marked final, then they are reassignable like any other variable. The confusion might come from the fact that top-level and class member _initializer_ expressions must be constant expressions - that is, they have to yield a deeply immutable object. But that's just the RHS of the assignment, after the program starts running the variables and fields can be reassigned.

      The constant expression requirement is so that the VM can load the program right up to the point where main() is ready to run, and then take a snapshot of memory and cache it. Any time the program is re-run, it can start from the snapshot.

      So in your case you could have a top-level var, static var, or class member initialized to null and then set in some initialize() function. Or, probably better, you can have a function that returns the sync function, and a setter as well. Then you don't need the explicit initialization step.

      Delete
    3. Ah, thanks that helps.

      I still don't think that the top-level var is going to work, though. If I declare `var sync = null` outside of main() in main.dart, that variable is not accessible from inside subsequent libraries that are pulled in via `#import()`.

      It might work inside a class -- as a static method or singleton. I'll play with that.

      Delete
    4. Yup, class variables / setters did the trick (could get top-level variables working): http://japhr.blogspot.com/2012/02/injecting-shared-behavior-in-dart.html

      *Huge* thanks for your help!

      Delete