Het visitor design pattern komt zo nu en dan boven drijven als mogelijke oplossing. Vaak blijkt het dan niet de juiste oplossing te zijn, simpelweg omdat de naam (visitor) verwarrend is. Vaak zijn de voorbeelden op het internet niet overtuigend en ontstaat de neiging om direct naar de problemen van het pattern te kijken.

Om een compleet beeld te krijgen start ik hier met het uitwerken van een concreet probleem. De initiële oplossing kan dan gerefactored worden tot het visitor pattern. Deze uiteindelijke oplossing dient dan als basis voor een verdere discussie.

 

Het probleem komt rechtstreeks uit het Design patterns boek van de Gang of Four, dus geen prijs voor originaliteit. Stel we hebben een lijst met artikelen en we willen de totale prijs van alle artikelen bepalen. Ieder artikel heeft een normale prijs en een kortings-prijs. Iedere week is er een andere selectie van artikelen wat in de aanbieding is. Nu volgt een compleet programma wat dit implementeert:

#include <cassert>
#include <iostream>
#include <memory>
#include <vector>

double total; // Foei!

struct Article {
  virtual double price() const = 0;
  virtual double discountPrice() const = 0;
};
typedef std::shared_ptr<Article> ObjectRef;

struct Glue : public Article {
  virtual double price() const {return 20.;};
  virtual double discountPrice() const {return 10.;};
};

struct Nail : public Article {
  virtual double price() const {return 2.;};
  virtual double discountPrice() const {return 1.;};
};

void collectObjects(std::vector<ObjectRef>& objects) {
  objects.push_back(ObjectRef(new Glue));
  objects.push_back(ObjectRef(new Nail));
}

void calculateTotalPrice(std::vector<ObjectRef>& articles) {
  total = 0.;
  for (auto it = articles.begin(); it != articles.end(); ++it) {
    if (dynamic_cast<Nail*>(it->get())) {
      total += (*it)->discountPrice();
    } else if (dynamic_cast<Glue*>(it->get())) {
      total += (*it)->price();
    } else {
      assert(false);
    }
  }
}

int main() {
  std::vector<ObjectRef> objects;
  collectObjects(objects);

  calculateTotalPrice(objects);
  std::cout << "Total = " << total << " (= 20. + 1.)\n";
  return 0;
}

Merk op het gebruik struct in plaats van class en wat c++11 constructies heeft als doel om het allemaal zo kort mogelijk te houden. Uiteindelijk is de hele opdracht geprogrammeerd in de functie calculateTotalPrice. Het is mooi dat het interface van Article gebruikt word, maar niet aangepast is. De dynamic_cast's zijn niet zo mooi en vaak een teken van een ontbrekende classen structuur. In feite is de situatie nog erger, want er zullen meerdere aanbiedingsacties zijn:

int main() {
  std::vector<ObjectRef> objects;
  collectObjects(objects);

  if (oddWeek()) {
    calculateHammerTimeTotalPrice(objects);
  } else {
    calculateGlueTimeTotalPrice(objects);
  }
  std::cout << "Total = " << total << " (= 20. + 1.)\n";
  return 0;
}

 Laten we dit probleem eerst aanpakken. Zo'n simpele if-statement kan vervangen worden door een classen structuur:

#include <cassert>
#include <iostream>
#include <memory>
#include <vector>

double total; // Foei!

struct Article {
  virtual double price() const = 0;
  virtual double discountPrice() const = 0;
};
typedef std::shared_ptr<Article> ObjectRef;

struct Glue : public Article {
  virtual double price() const {return 20.;};
  virtual double discountPrice() const {return 10.;};
};

struct Nail : public Article {
  virtual double price() const {return 2.;};
  virtual double discountPrice() const {return 1.;};
};

struct PriceCalculator {
  virtual void calculatePrice(const ObjectRef& object) = 0;
};
typedef std::shared_ptr<PriceCalculator> ActionRef;

struct HammerTimePriceCalculator : public PriceCalculator {
  virtual void calculatePrice(const ObjectRef& object);
};

struct GlueTimePriceCalculator : public PriceCalculator {
  virtual void calculatePrice(const ObjectRef& object);
};

void HammerTimePriceCalculator::calculatePrice(const ObjectRef& object) {
  if (dynamic_cast<Nail*>(object.get())) {
    total += object->discountPrice();
  } else if (dynamic_cast<Glue*>(object.get())) {
    total += object->price();
  } else {
    assert(false);
  }
}

void GlueTimePriceCalculator::calculatePrice(const ObjectRef& object) {
  if (dynamic_cast<Nail*>(object.get())) {
    total += object->price();
  } else if (dynamic_cast<Glue*>(object.get())) {
    total += object->discountPrice();
  } else {
    assert(false);
  }
}

void collectObjects(std::vector<ObjectRef>& objects) {
  objects.push_back(ObjectRef(new Glue));
  objects.push_back(ObjectRef(new Nail));
}

void selectCorrectAction(ActionRef& action) {
  action = ActionRef(new HammerTimePriceCalculator);
}

void calculateTotalPrice(std::vector<ObjectRef>& articles, ActionRef& action) {
  total = 0.;
  for (auto it = articles.begin(); it != articles.end(); ++it) {
    action->calculatePrice(*it);
  }
}

int main() {
  std::vector<ObjectRef> objects;
  collectObjects(objects);

  ActionRef action;
  selectCorrectAction(action);

  calculateTotalPrice(objects, action);
  std::cout << "Total = " << total << " (= 20. + 1.)\n";
  return 0;
}

calculateTotalPrice bevat nu alleen nog maar een loop over de artikelen. De juiste calculatePrice word nu aangeroepen door de nieuwe classen structuur. Zoals boven al aangehaald, zouden we dit ook willen doen voor de dynamic_cast's. Helaas(?) kan C++ dit niet in een keer:

struct HammerTimePriceCalculator : public PriceCalculator {
  virtual void calculatePrice(const Glue& object);
  virtual void calculatePrice(const Nail& object);
};

C++ gaat nooit runtime een functie uitkiezen aan de hand van een parameter. Dit brengt ons bij single-dispatch vs. multiple-dispatch (https://en.wikipedia.org/wiki/Dynamic_dispatch#Single_and_multiple_dispatch). Toch kun je dit in C++ met een trucje voor elkaar krijgen:


#include <iostream>
#include <memory>
#include <vector>

double total; // Foei!
class Visitor;

struct Article {
  virtual double price() const = 0;
  virtual double discountPrice() const = 0;
  virtual void accept(Visitor& visitor) = 0;
};
typedef std::shared_ptr<Article> ObjectRef;

struct Glue : public Article {
  virtual double price() const {return 20.;};
  virtual double discountPrice() const {return 10.;};
  virtual void accept(Visitor& visitor);
};

struct Nail : public Article {
  virtual double price() const {return 2.;};
  virtual double discountPrice() const {return 1.;};
  virtual void accept(Visitor& visitor);
};

struct Visitor {
  virtual void visit(Nail& nail) = 0;
  virtual void visit(Glue& glue) = 0;
};
typedef std::shared_ptr<Visitor> ActionRef;

struct HammerTimePriceCalculator : public Visitor {
  virtual void visit(Nail& nail) {total += nail.discountPrice();};
  virtual void visit(Glue& glue) {total += glue.price();};
};

struct GlueTimePriceCalculator : public Visitor {
  virtual void visit(Nail& nail) {total += nail.price();};
  virtual void visit(Glue& glue) {total += glue.discountPrice();};
};

// Dit is een dependency probleem, zie acyclic visitor
void Nail::accept(Visitor& visitor) {visitor.visit(*this);}
void Glue::accept(Visitor& visitor) {visitor.visit(*this);}

void collectObjects(std::vector<ObjectRef>& objects) {
  objects.push_back(ObjectRef(new Glue));
  objects.push_back(ObjectRef(new Nail));
}

void selectCorrectAction(ActionRef& action) {
  action = ActionRef(new HammerTimePriceCalculator);
}

void calculateTotalPrice(std::vector<ObjectRef>& articles, ActionRef& action) {
  total = 0.;
  for (auto it = articles.begin(); it != articles.end(); ++it) {
    (*it)->accept(*action);
  }
}

int main() {
  std::vector<ObjectRef> objects;
  collectObjects(objects);

  ActionRef action;
  selectCorrectAction(action);

  calculateTotalPrice(objects, action);
  std::cout << "Total = " << total << " (= 20. + 1.)\n";
  return 0;
}

De dynamic_cast's zijn nu vervangen door de accept() functie in de classen hierarchie. Door heen en weer te gaan naar de accept functie kan het concrete artikel bepaald worden en kan dus de priceCalculator de correcte prijs bepalen. 

Nu hebben we door een aantal refactoringen het visitor pattern geimplementeerd. De eerste tegenwerping is dat we de interface van Article toch hebben aangepast met de accept() functie. Deze functie kan echter herbruikt worden door andere visitors, immers het doel van deze functie is alleen maar om een (bewuste) beperking van C++ heen te werken. Een andere belangrijke opmerking is het om te zien dat de visitor alleen het publieke interface van Article gebruikt. Als dit niet zo zou zijn, zou de integriteit van Article twijfelachtig zijn. Aan de hand van deze eerste implementatie gaan we kijken naar de eigenschappen van dit pattern.

Comments powered by CComment