Wednesday, November 18, 2015

Locking Down Flyweights Mirrors in Dart


At the risk of over-examining a topic beyond all reason, I continue tonight to explore Dart mirrors as a means of obtaining concrete Flyweight objects. Hey, if you can't over-examine a topic when blogging every day, when can you?

I very much like yesterday's mirror approach. In my coffee shop example, the CoffeeFlavor reference class is responsible for identifying concrete flyweights. The calling code behaves just like it would if it were getting a generic CoffeeFlavor instance:
  void order(name, sizeName, {who, fooFoo}) {
    var flavor = new CoffeeFlavor(name);
    int size = sizes[sizeName];
    _orders.add(new Order(flavor, size, who, fooFoo));
  }
But the factory constructor in CoffeeFlavor is looking up concrete classes (e.g. Cappuccino, Latte, etc). in its internal cache:
class CoffeeFlavor {
  // ...
  factory CoffeeFlavor(name) =>
    _cache.putIfAbsent(name, () =>
        classMirrors[new Symbol(name)].
           newInstance(new Symbol(''), []).
           reflectee
    );
  // ...
}
If present, the previously created concrete instance — the flyweight — is returned. If it is not already present, then the concrete class is looked up in the current library and used to create the flyweight. This approach has the dual benefit of keeping the client code clean (it thinks it is creating an instance of CoffeeFlavor) and keeping the concrete classes simple.

The concrete classes need only define intrinsic properties of particular coffee flavors:
class Cappuccino implements CoffeeFlavor {
  String get name => 'Cappuccino';
  double get profitPerOunce => 0.35;
}
No metadata required (I may explore that tomorrow, though). No tomfoolery needed just to support the factory. It's all clean.

I think I can make this even better by restricting the map of classMirrors to just classes that implement CoffeeFlavor. Just because I am using mirrors doesn't give me license to be obscene. This is statically typed Dart, not some loosey-goosey dynamically typed language.

That turns out to be a little harder than expected. First off, I need to grab the list of all declarations from the local mirror:
class CoffeeFlavor {
  // ...
  static Map _allDeclarations = currentMirrorSystem().
      findLibrary(#coffee_shop).
      declarations;
  // ...
}
There is no pairs accessor for Dart maps, so I have to iterate over all of the keys in the _allDeclarations map. From there, I filter only class mirror declarations, and only classes that implement the CoffeeFlavor interface. Lastly, I reassemble the map via a fold():
class CoffeeFlavor {
  // ...
  static Map _allDeclarations = currentMirrorSystem().
      findLibrary(#coffee_shop).
      declarations;

  static Map classMirrors = _allDeclarations.
    keys.
    where((k) => _allDeclarations[k] is ClassMirror).
    where((k) =>
      _allDeclarations[k].superinterfaces.contains(reflectClass(CoffeeFlavor))
    ).
    fold({}, (memo, k) => memo..[k]= _allDeclarations[k]);
  // ...
}
That's a little verbose. It does the trick, but I am unsure if it is worth all of the noise.

Now that I think about it, it would be nice if this could work across libraries. My coffee_shop library can define a bunch of coffee flavor classes, but it would be helpful to support coffee flavor classes in the main code file as well:
class Mochachino implements CoffeeFlavor {
  String get name => "Mochachino";
  double get profitPerOunce => 0.3;
}

main() {
  var shop = new CoffeeShop()
    ..order('Cappuccino', 'large', who: 'Fred', fooFoo: 'Extra Shot')
    ..order('Espresso',   'small', who: 'Bob')
    ..order('Frappe',     'large', who: 'Alice')
    ..order('Frappe',     'large', who: 'Elsa', fooFoo: '2 Percent Foam')
    ..order('Coffee',     'small')
    ..order('Coffee',     'medium', who: 'Chris')
    ..order('Mochachino', 'large', who: 'Joy');
  // ...
}
To make that work, I need to rework the _allDeclarations assignment. Instead of looking in the current, coffee_shop library, I have to look across all libraries:
class CoffeeFlavor {
  // ...
  static Map _allDeclarations = currentMirrorSystem().
      libraries.
      values.
      fold({}, (memo, library) => memo..addAll(library.declarations));
  // ...
}
Again, I have to work around the lack of a pairs iterable on Map. That aside, this works nicely. My customers can order plenty of Mochachinos at the local coffee shop even if corporate does not support them yet.

There is definitely some power in all these Dart mirrors. That power may not always be necessary, but it is nice to know that it is there if I need it.

(Play with this code on DartPad: https://dartpad.dartlang.org/d103fcc8e1cbf371fa04)


Day #7

No comments:

Post a Comment