Monday, July 7, 2014

Adding a Visitor Really is That Easy


I have felt the pain of adding new nodes and structure in the Dart version of the Visitor Pattern, but I have yet to try the opposite. Tonight, I try adding a new Visitor in the hopes that the pattern really does make this easy.

All indications are that this should be nearly trivial. The Gang of Four book says the pattern “lets you define a new operation without changing the classes of the elements on which it operates.” I have no reason to doubt this. Still, I need to give it whirl for the sake of completeness in Design Patterns in Dart.

The data structure remains a collection of work stuff (equipment and apps):
  var work_stuff = new InventoryCollection([
    mobile()..apps = [
      app('2048', price: 10.0),
      app('Pixel Dungeon', price: 7.0),
      app('Monument Valley', price: 4.0)
    ],
    tablet()..apps = [
      app('Angry Birds Tablet Platinum Edition', price: 1000.0)
    ],
    laptop()
  ]);
All of the inventory in that data structure implement the same Inventory interface:
abstract class Inventory {
  String name;
  Inventory(this.name);

  double netPrice;
  double discountPrice() => netPrice;

  void accept(vistor);
}
For the purposes of the Visitor Pattern, the accept() method at the end is the important piece. For each type of inventory, the class defines this method such that it calls a corresponding method in the visitor. The Laptop class defines accept() such that it invokes visitLaptop() in the visitor:
class Laptop extends Equipment {
  Laptop(): super('Laptop');
  double netPrice = 1000.00;
  double discountPrice() => netPrice * .9;
  void accept(visitor) { visitor.visitLaptop(this); }
}
Mobile's accept() calls visitMobile(). And so on.

Since every node in the data structure will call the corresponding method in the visitor, the visitor can perform operations across the entire structure. All of this is already in place and I have a total price Visitor that accumulates the total price—arcane business rules and all:
class PricingVisitor extends InventoryVisitor {
  double _totalPrice = 0.00;

  double get totalPrice => _totalPrice;

  void visitMobile(i) { _totalPrice += i.netPrice; }
  void visitTablet(i) { _totalPrice += i.discountPrice(); }
  void visitLaptop(i) { _totalPrice += i.discountPrice(); }

  void visitApp(i) { _totalPrice += 0.5 * i.discountPrice(); }
}
The question before me is, is it really easy to add a new visitor?

To answer that, I start at the top. I would like a visitor that counts the number of apps across types of inventory. Regardless of whether the app is installed on a tablet or mobile, I would like to be able to ask:
  var counter = new TypeCountVisitor();
  work_stuff.accept(counter);
  print('I have ${counter.apps} apps!');
And it turns out that the GoF really were telling the truth. I do not need to make a single change to the node structure to obtain this information. All I need is a TypeCountVisitor class that counts the individual types in a Map:
class TypeCountVisitor extends InventoryVisitor {
  Map<String,int> _count = {
    'mobile': 0,
    'tablet': 0,
    'laptop': 0,
    'app': 0
  };

  int get mobiles => _count['mobile'];
  void visitMobile(i) { _count['mobile']++; }

  int get tablets => _count['tablet'];
  void visitTablet(i) { _count['tablet']++; }

  int get laptops => _count['laptop'];
  void visitLaptop(i) { _count['laptop']++; }

  int get apps => _count['app'];
  void visitApp(i) { _count['app']++; }
}
If I run that against my “work stuff”, I find that I currently have 4 apps as expected:
$ ./bin/cost.dart
I have 4 apps!
Nice!

I can already see that it would be difficult to add another piece of inventory (desktops, chargers, etc.), but I knew this going into the exercise. Adding new types of nodes to the data structure is hard—I would have to add new visitXXX() methods to every visitor in play, not to mention needing to define the node types themselves. The visitor is ill-suited to that kind of work. But adding new operations on a fairly static data structure? That is a joy.


Day #115

No comments:

Post a Comment