Tuesday, February 14, 2012

Injecting Shared Behavior in Dart

‹prev | My Chain | next›

At the B'more on Rails meetup tonight, Nick Gauthier, my Recipes with Backbone co-author, included an interesting discussion on frameworks vs. libraries. He pointed out that frameworks generally call user written application code whereas libraries encourage application code to call library code (usually through inheritance). It occurs to me that this is the crux of my recent struggles with my Dart MVC framework—I am having a hell of a time injecting a sync() method for my model and collection classes to call.

I had planned on giving this a miss, but, partly egged on by Nick and partly by comments from Justin Fagnani in last night's post, I am going to try again.

My first attempt tonight focuses on using a global variable to hold this information. I do not expect to stick with a global, but just hope to use it as a proof of concept. First, I establish a "internal" version of a variable that will point to my sync function:
var internal_sync = null;
Then I define an externally facing setter function for the internal version:
var internal_sync = null;
set sync(fn) {
  internal_sync = fn;
}
In my framework, internal_sync would have a default Ajax implementation, but the developer could inject different behavior by setting a different function via the external sync= setter.

And this works, to a certain extent. In my main() entry point, I can set sync and invoke the new behavior:
var internal_sync = null;
set sync(fn) {
  internal_sync = fn;
}

main() {
  sync = (arg) {
    print("[new_sync] $arg");
    return "hip new behavior";
  };
  print("sync: ${internal_sync('Injected, yo')}");
}
(try.dartlang.org)

Inside main() I set the sync= setter to a function that prints out an argument and returns a "hip new behavior". I then invoke the function that is now referred to by the internal_sync global variable. And it works. I see the set function being called and I see the "hip new behavior":
[new_sync] Injected, yo
sync: hip new behavior
The problem is that this internal_sync variable is not available inside libraries. For instance, the Collections.Comics.dart library is imported and used in main() after the updated internal_sync is set:
#import('Collections.Comics.dart', prefix: 'Collections');
// ...

var internal_sync = null;
set sync(fn) {
  internal_sync = fn;
}

main() {
  sync = (arg) {
    print("[new_sync] $arg");
    return "hip new behavior";
  };
  print("sync: ${internal_sync('Injected, yo')}");

  var my_comics_collection = new Collections.Comics();
 
  my_comics_collection.fetch();
  // ...
}
In turn, the Comics class imports and extends HipsterCollection from my framework:
#import('HipsterCollection.dart');

class Comics extends HipsterCollection {
  // ...
}
And, in HipsterCollection, I try to access the global internal_sync variable:
class HipsterCollection implements Collection {
  // ...
  fetch() {
    print("[fetch] sync: ${internal_sync}");
    // ...
  }
}
Unfortunately, I am denied:
Exception: NoSuchMethodException - receiver: 'Instance of 'Comics'' function name: 'get:internal_sync' arguments: []]
Stack Trace:  0. Function: 'Object.noSuchMethod' url: 'bootstrap' line:665 col:3
 1. Function: 'HipsterCollection.fetch' url: 'http://localhost:3000/scripts/HipsterCollection.dart' line:54 col:41
 2. Function: '::main' url: 'http://localhost:3000/scripts/main.dart' line:23 col:29
I get similar results if I try to treat this internal_sync as a function.

It is not possible to declare variables before #import() statements in Dart, nor is it possible to include code via #source() before #import(). So, if the problem is that the global is declared too late, then I am out of luck.

I switch tactics at this point. If a global variable will not work, perhaps a class methods will. So I define a HipsterSync class in HipsterSync.dart with a familiar class variable and class setter:
#library('Sync layer for the Hipster MVC');

class HipsterSync {
  static var internal_sync;

  static set sync(fn) {
    internal_sync = fn;
  }
}
I can then import and set HipsterSync.sync= in main():
// ...
#import('HipsterSync.dart');

main() {
  HipsterSync.sync = (arg) {
    print("[new_sync] $arg");
    return "hip new behavior";
  };
  print("sync: ${HipsterSync.internal_sync('Injected, yo')}");
  // ...
}
Finally, I import and attempt to use HipsterSync in my collection base class:
// ...
#import('HipsterSync.dart');

class HipsterCollection implements Collection {
  // ...
  fetch() {
    print("[fetch] sync: ${HipsterSync.internal_sync('fetch, yo')}");
    // ...
  }
  // ...
}
And... that does the trick! In the Dart console, I see the output from main():
[new_sync] Injected, yo
sync: hip new behavior
Followed immediately by the output from the newly set HipsterSync.sync when invoked by the collection's fetch() method:
[new_sync] fetch, yo
[fetch] sync: hip new behavior
It took me a while to figure that out (and much thanks to Justin Fagnani for pointing me in the right direction), but I am finally able to inject model and collection syncing behavior into my MVC framework. Using static methods to set and hold this behavior also ensures that the behavior can be shared between both the collection and model classes. I am excited.


Day #296

No comments:

Post a Comment