Saturday, July 5, 2014

Visitor with a Little Composite in Dart


Translating the Gang of Four example code for the Visitor Pattern into Dart went off without a hitch. Even without the complex data structure that really demonstrate the power the Visitor, the example is already illuminating.

That said, to make it useful in Design Patterns in Dart, I should explore more complex data structures. Thankfully, I do not have to work too hard for this. Or at all. The GoF example inlcuded a Composite Pattern from the outset for just this reason.

My purpose in making the data structure more complex is more than simply putting the pattern through its paces. I am also eager to experience where the pain lies in adding new types and structure. So I start with the main() entry point of my application which has a list of business inventory to which it supplies a Visitor that accumulates price:
  // ...
  var work_stuff = [
    new Mobile(),
    new Tablet(),
    new Laptop()
  ];

  var cost = new PricingVisitor();
  work_stuff.forEach((e){ e.accept(cost); });
  print('Cost of work stuff: ${cost.totalPrice}.');
  // ...
For the composite in this example, I will use “apps” on mobiles and tablets. Everyone knows that apps are not necessary on laptops, everything that is necessary on them comes from apt-get after all. So my work stuff might wind up looking like:
  // ...
  var work_stuff = [
    new Mobile()
      ..apps = [
          new App('2048')..netPrice = 10.0,
          new App('Pixel Dungeon')..netPrice = 7.0,
          new App('Monument Valley')..netPrice = 4.0
        ],
    new Tablet()
      ..apps = [
          new App('Angry Birds Tablet Platinum Edition')..netPrice = 1000.0
        ],
    new Laptop()
  ];
  // ...
That is three relative cheap—but necessary—work apps on my mobile and one very expensive app on the tablet.

The usage of PricingVisitor remains unchanged:
  var cost = new PricingVisitor();
  work_stuff.forEach((e){ e.accept(cost); });
  print('Cost of work stuff: ${cost.totalPrice}.');
That is a nice advantage of the Visitor pattern.

Also nice is that the Visitor class itself does not need to change much. I am still working with the same equipment from last night. The only new thing here is an “app.” In the Visitor class, that means that I will now need a visitApp() method:
abstract class InventoryVisitor {
  void visitMobile(Mobile i);
  void visitTablet(Tablet i);
  void visitLaptop(Laptop i);
  void visitApp(App i);
}
The concrete PricingVisitor, which accumulates the total cost of all inventory, can then do what it does best—applying a special rule for individual types. In this case, I only accumulate half the value of apps. In this case, I accumulate only half the value of apps:
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(); }
}
So far, so good. There really has not been any pain just yet. What is a pain is adding the new nodes—the composite and the App. Well, not so much a pain, but tedious:
abstract class Inventory {
  String name;
  Inventory(this.name);

  double netPrice;
  double discountPrice() => netPrice;

  void accept(vistor);
}

class App extends Inventory {
  App(name): super(name);
  void accept(visitor) { visitor.visitApp(this); }
}

abstract class EquipmentWithApps extends Equipment {
  EquipmentWithApps(name): super(name);
  List<App> apps = [];
}
Even with that, I still have to add concrete composites like the mobile phone class:
class Mobile extends EquipmentWithApps {
  Mobile(): super('Mobile Phone');
  double netPrice = 350.00;
  void accept(visitor) {
    apps.forEach((app) { app.accept(visitor); });
    visitor.visitMobile(this);
  }
}
This is about as complex as things get tonight. The Mobile piece of inventory composites other kinds of inventory, apps. As such, when it accepts a visitor, it is responsible for telling each app that it needs to accept the visitor as well. Since App accepts visitors and the Visitor implements visitApp(), the pattern circle is complete.

I can calculate the costs for all of my inventory, apps and all:
$ ./bin/cost.dart
Cost of work stuff: 2160.5.
I begin to appreciate the GoF equipment / inventory example. I helps to highlight where the pain is. As they noted, adjusting the Visitor class is relatively lightweight. But adding a node or two really involves much work. While it might not be complex work—the pattern is one of those that emphasizes encapsulation—each new node requires the node class as well as new visit methods in each concrete visitor in use.

I expect that adding a new visitor or tweaking the data structure (without adding a new node type) are the kinds of operations that require almost no work. I will pick back up exploring that tomorrow.



Day #113

No comments:

Post a Comment