Friday, July 4, 2014

Simple Visitor Pattern in Dart


Digging through the Visitor pattern code in polymer_expressions was supremely illuminating yesterday. But I feel that I need some actual code to play with tonight.

The point of this continues to help me to determine which questions I need to ask as I explore patterns for Design Patterns in Dart. This is background research for my eventual research. The code review from yesterday served two purposes: looking at a solid implementation of the pattern in Dart and identifying the pattern in the wild. The former is purpose unto itself. The latter I need because the Gang of Four book included discussion of each pattern in the wild, which is something that I appreciate.

But there is nothing like the feel of real code.

Not wanting to get involved in abstract syntax trees, which seem particularly well suited to the Visitor pattern, I instead translate the example from the GoF book into Dart. I start with an abstract Equipment class:
abstract class Equipment {
  String name;
  Equipment(this.name);

  int watt;
  double netPrice;
  double discountPrice() => netPrice;

  void accept(vistor);
}
There is nothing too fancy here, which is partially the point of Visitor—it keeps the nodes in a data structure clean of unnecessary code. Here, I declare that equipment in my list of inventory will have a name, the number of watts that it consumes, a price and, optionally, a discount price.

There is one method that does not have anything to do with the intrinsic meaning of what equipment is or does. The accept() method will take some kind of visitor in order to do… something with it. What that “something” is will depend on the purpose of the visitor. Before building a visitor, I declare a few pieces of equipment. With apologies to the Gang of Four, I opt for newer types of equipment (what the heck is a “floppy disk”?):
class Mobile extends Equipment {
  Mobile(): super('Mobile Phone');
  double netPrice = 350.00;
  void accept(visitor) { visitor.visitMobile(this); }
}

class Tablet extends Equipment {
  Tablet(): super('Tablet');
  double netPrice = 400.00;
  void accept(visitor) { visitor.visitTablet(this); }
}

class Laptop extends Equipment {
  Laptop(): super('Laptop');
  double netPrice = 1000.00;
  double discountPrice() => netPrice * .9;
  void accept(visitor) { visitor.visitLaptop(this); }
}
Each piece of modern equipment has a name and price. To mix things up, I declare that laptops have discount prices of 90% of the net price while other pieces have no discount. The important aspect of each of these is that the concrete equipment classes accept a visitor and then call the method in the visitor that corresponds to the type of equipment. That is, the Laptop class calls visitLaptop() in the visitor.

The implication is that, for each type of equipment that I have, all visitors will need to have a corresponding “visit” method. Since I currently have three possible types of equipment, that means that my equipment visitors will have to declare at least theses three visit methods:
abstract class EquipmentVisitor {
  void visitMobile(Mobile m);
  void visitTablet(Tablet t);
  void visitLaptop(Laptop l);
}
The visitor example used in the GoF was a cost calculator. If I have a list of work stuff that looks like:
  var work_stuff = [
    new Mobile(),
    new Tablet(),
    new Laptop()
  ];
Then I ought to be able to report on the total cost. If the report only needs to include the net cost, a simple iteration across this list would suffice. But what if there are different reporting rules for each kind of equipment? What if I need to report the net price for mobiles, but discount price for tablets and laptops? Conditionals inside iterators are ugly. Enter the PricingVisitor:
class PricingVisitor extends EquipmentVisitor {
  double _totalPrice = 0.00;

  double get totalPrice => _totalPrice;

  void visitMobile(e) { _totalPrice += e.netPrice; }
  void visitTablet(e) { _totalPrice += e.discountPrice(); }
  void visitLaptop(e) { _totalPrice += e.discountPrice(); }
}
If I have three mobiles, each will call visitMobile() ensuring that I get an accurate count. If I do not require a tablet, visitTablet() will never be called and will not count toward the total. As long as each piece of work equipment is told to accept the PricingVisitor, I will get my total price. Best of all, any variations of rules are grouped in one place rather than scattered throughout a number of classes.

To put this is use, I have the main() entry point for my code invoke accept() for each piece of equipment:
main() {
  var cost = new PricingVisitor();

  var work_stuff = [
    new Mobile(),
    new Tablet(),
    new Laptop()
  ];

  work_stuff.forEach((e){ e.accept(cost); });

  print('Cost of work stuff: ${cost.totalPrice}.');
}
The result:
$ /bin/cost.dart
Cost of work stuff: 1650.0.
That was rather nice—and easy.

There is still much to explore with the Visitor—difference implementations, additional visitors, more complex data structures than simple lists—but this GoF example seems promising even when translated to modern times.


Day #112

No comments:

Post a Comment