Tuesday, November 17, 2015

Concrete Flyweight Objects with Dart Mirrors


I worried that last night's post on concrete Flyweight objects in Dart would be too easy to warrant a post. Then I actually tried to write the code.

So it turns out that we are not allowed to create compile-time static variables in Dart—and haven't been allowed for quite some time. Well, we are allowed, but they will not be evaluated until explicitly invoked. This thwarted my plan to use these static variable declarations to register themselves as flyweight implementations.

But the challenge remains. I would very much like to create concrete instances of flyweight objects without resorting to ickery like case statements. Tonight, I explore the dreaded mirrors to accomplish this.

Sidenote: I understand that there is a StackOverflow topic on this very question. I am purposefully not reading that discussion so that I can explore the topic fresh here. The answer on StackOverflow is likely far superior to mine. So let's get to mine!

In my current coffee shop example, I would like to register orders along the lines of:
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');
Over the course of the day, I expect there to be a large number of orders so I want to minimize the number of objects retained in memory. Enter the flyweight.

Specifically, the intrinsic properties of a coffee flavor like “Cappuccino” do not change. The size, the orderer, and silly additional instructions change with each order, but a Cappuccino stays a Cappuccino all day long. So I make this a flyweight object.

Thanks to Dart's factory constructors, I do not even require a factory class to accomplish this. From the outside, creating new CoffeeFlavor instances looks just like creating any other object:
class CoffeeShop {
  void order(name, sizeName, {who, fooFoo}) {
    var flavor = new CoffeeFlavor(name); // Looks like a regular constructor!!!
    int size = sizes[sizeName];
    _orders.add(new Order(flavor, size, who, fooFoo));
  }
  // ...
}
Before introducing concrete classes for the various coffee flavors, this factory constructor simply looked up a previously used flyweight in a cache:
class CoffeeFlavor {
  // ... 
  factory CoffeeFlavor(name) =>
      _cache.putIfAbsent(name, () => new CoffeeFlavor._(name));
  // ...
}
Instead of a vanilla CoffeeFlavor object, I want an actual instance of Cappuccino:
class FakeCoffee implements CoffeeFlavor {
  String get name => "Fake Coffee";
  double get profitPerOunce => 0.0;
}
For that, I think I will need mirrors. I start by importing dart:mirrors:
library coffee_shop;

import 'dart:mirrors';
Next, I need to find a mirror for the current library (coffee_shop):
      var m = currentMirrorSystem().findLibrary(#coffee_shop);
In the list of declarations of my library mirrors, I can get a class mirror for a given name (e.g. "Cappuccino"):
      var c = m.declarations[new Symbol(name)];
Finally, I can get a new instance of that class:
      return c.newInstance(new Symbol(''), []).reflectee;
Putting this altogether, my factory constructor becomes:
class CoffeeFlavor {
  // ...
  factory CoffeeFlavor(name) =>
    _cache.putIfAbsent(name, (){
      var m = currentMirrorSystem().findLibrary(#coffee_shop);

      var c = m.declarations[new Symbol(name)];
      return c.newInstance(new Symbol(''), []).reflectee;
    });
  // ...
}
I still use a putIfAbsent() to build up the cache—only now it creates concrete instances from mirrors.

There are several things to like about this approach. The client context is still blissfully unaware that the Flyweight pattern is in play—it still creates new CoffeeFlavor objects only to receive a flyweight subclass instead. I also like that this approach returns concrete instances of flyweight objects. I may not always need to do so, but it is good to know that this is possible. Lastly, I appreciate that I can accomplish this without a case statement.

There are drawbacks, however, to this approach. The Flyweight is intended to save memory, ideally to speed the code up. This approach relies entirely on mirrors which are not exactly high-performance beasties. That said, once the cache is populated, reflection is no longer used so the initial performance hit of mirrors could easily be overcome by later optimizations. It all depends on the context in which the code is being used.

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


Day #6

No comments:

Post a Comment