Tuesday, November 24, 2015

Reflectable Subtypes and the Flyweights that Love Them


Tonight, I continue my exploration of the Reflectable package as applied to the Flyweight Pattern.

I am quite happy with the current state of the Reflectable-based code. I have defined a flavor constant that inherits from Reflectable so that it can annotate my code:
class Flavor extends Reflectable {
  const Flavor()
    : super(newInstanceCapability);
}
const flavor = const Flavor();
I then use this constant to mark the individual concrete flyweight objects—in the case of this example, coffee flavors for my coffee shop:
@flavor
class Cappuccino implements CoffeeFlavor {
  String get name => 'Cappuccino';
  double get profitPerOunce => 0.35;
}

@flavor
class Espresso implements CoffeeFlavor {
  String get name => 'Espresso';
  double get profitPerOunce => 0.15;
}
It seems a bit of a nuisance to have to annotate every concrete flyweight class like this. Thanks to a pointer from Erik Ernst in yesterday's comments, I can eliminate that nuisance.

Reflectable works by only applying a limited subset of built-in dart:mirrors capabilities. Currently, I only use newInstanceCapability so that the CoffeeFlavor factory constructor can create new instances of the concrete flyweight objects:
class CoffeeFlavor {
  // ...
  factory CoffeeFlavor(name) {
    return _cache.putIfAbsent(name, () =>
        classMirrors[name].newInstance('', []));
  }
  // ...
}
Reflectable objects like my flavor constant come with the built-in ability to find all classes annotated by the object, so the classMirrors map is easily assembled from flavor:
class CoffeeFlavor {
  // ...
  static Map classMirrors = flavor.
    annotatedClasses.
    fold({}, (memo, c) => memo..[c.simpleName]= c);

  factory CoffeeFlavor(name) { /* ... */ }
  // ... 
}
Getting back to Erik's suggestion, I can add a new capability to my reflectable Flavor class—the subtypeQuantifyCapability:
class Flavor extends Reflectable {
  const Flavor()
    : super(newInstanceCapability, subtypeQuantifyCapability);
}

const flavor = const Flavor();
What this means is that I can move all of the @flavor annotations from the concrete flyweight CoffeeFlavor classes and instead place one on the CoffeeFlavor class itself:
@flavor
class CoffeeFlavor {
  // ...
  static Map classMirrors = flavor.
    annotatedClasses.
    fold({}, (memo, c) => memo..[c.simpleName]= c);

  factory CoffeeFlavor(name) { /* ... */ }
  // ... 
}

class Cappuccino implements CoffeeFlavor {
  String get name => 'Cappuccino';
  double get profitPerOunce => 0.35;
}

class Espresso implements CoffeeFlavor {
  String get name => 'Espresso';
  double get profitPerOunce => 0.15;
}

// More concrete flyweight classes here...
With that, the annotatedClasses includes every class that extends or implements the annotated CoffeeFlavor class. I have the same exact code and functionality, but with a single annotation instead of one annotation per concrete class.

Furthermore, this works across libraries. If I define CoffeeFlavor and the various concrete classes in a library, but define another concrete class in the main entry point:
// The library with the factory and flyweights:
import 'package:flyweight_code/coffee_shop.dart';

// Concrete flyweight outside the library
class Mochachino implements CoffeeFlavor {
  String get name => "Mochachino";
  double get profitPerOunce => 0.3;
}

main() {
  // Go about coffee shop business here...
}
It still works. That is, the flavor constant is still aware of this Mochachino class as a "sub type".

I had thought to originally try to bend Reflectable to the point that it could mimic my original implementation. In there, I use dart:mirrors to search through all code to find classes that implement CoffeeFlavor. That seemed the most explicit implementation of what I needed—a list of available concrete classes without much overhead.

But I have to admit that Reflectable with subtypeQuantifyCapability accomplishes exactly the same thing with almost no additional overhead—just a constant or two. The resulting code is easier to read and the generated JavaScript (as I found last night) is much smaller. This is a win all around.


Day #13

No comments:

Post a Comment