Dit is het vervolg van Visitor. Het voorbeeld uit het Design patterns boek van de Gang of Four is bijzonder goed. Een ander voorbeeld word gebruikt om een acyclic visitor uit te leggen in http://www.objectmentor.com/resources/articles/acv.pdf. Dit artikel is een verder uitstekende uitleg van een van de problemen van het visitor pattern. Echter is het gekozen voorbeeld in mijn ogen ongelukkig. Namelijk het configureren van modems voor verschillende platforms.

Het configureren van een klasse zal vaak betrekking hebben op de private members van een klasse. Het visitor pattern is hiervoor niet goed geschikt. Immers de visitor is een externe klasse die alleen het publieke interface kan gebruiken. Nu zijn er natuurlijk allerlei manieren om hier omheen te werken. Bijvoorbeeld extra getters en setters toevoegen op private members, een wel erg grote concessie. Of de visitor een friend maken, een nog grotere concessie. Geen van de opties zijn erg aantrekkelijk.

 

Als we nu even het visitor pattern loslaten en proberen het gestelde probleem op te lossen. We willen modems configureren voor verschillende platforms. Een eerste optie zou dan kunnen zijn om een event structuur te gebruiken:

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

struct ConfigureEvent;

struct Modem {
  virtual void configure(ConfigureEvent* event) = 0;
};
typedef std::shared_ptr<Modem> ObjectRef;

struct HayesSmartmodem : public Modem {
  virtual void configure(ConfigureEvent* event);
};

struct USRobotics : public Modem {
  virtual void configure(ConfigureEvent* event);
};

struct ConfigureEvent {
   virtual ~ConfigureEvent(){}
};
struct ConfigureForWindowsEvent : public ConfigureEvent {};
struct ConfigureForLinuxEvent : public ConfigureEvent {};
typedef std::shared_ptr<ConfigureEvent> ActionRef;

void HayesSmartmodem::configure(ConfigureEvent* event){
  if (dynamic_cast<ConfigureForWindowsEvent*>(event)) {
    std::cout << "configure Hayes for Windows\n";
  } else if (dynamic_cast<ConfigureForLinuxEvent*>(event)) {
    std::cout << "configure Hayes for Linux\n";
  } else {
    assert(false && "Unknown platform for Hayes");
  }

}

void USRobotics::configure(ConfigureEvent* event){
  if (dynamic_cast<ConfigureForWindowsEvent*>(event)) {
    std::cout << "configure USRobotics for Windows\n";
  } else if (dynamic_cast<ConfigureForLinuxEvent*>(event)) {
    std::cout << "configure USRobotics for Linux\n";
  } else {
    assert(false && "Unknown platform for USRobotics");
  }

}

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

void determineAction(ActionRef& actionRef) {
  actionRef = ActionRef(new ConfigureForWindowsEvent);
}

void applyActionToAllObjects(std::vector<ObjectRef>& objects, ActionRef& actionRef) {
  for (std::vector<ObjectRef>::iterator it = objects.begin(); it != objects.end(); ++it) {
    (*it)->configure(actionRef.get());
  }
}

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

  ActionRef actionRef;
  determineAction(actionRef);

  applyActionToAllObjects(objects, actionRef);
  return 0;
}

 

In dit voorbeeld zijn er twee concrete modems onder een abstracte modem klasse. Bovendien zijn er twee concrete events (Windows, Linux) onder een generieke event klasse. Het probleem zit in de configure functies. Hier kunnen we niet garanderen dat alle events afgehandeld worden, een runtime assert vangt het hier op. Als we ervan uitgaan dat elke modem zichzelf moet configureren en op elk platform moet werken, dan willen we eigenlijk het volgende:

struct Modem {
  virtual void configure(ConfigureForWindowsEvent* event) = 0;
  virtual void configure(ConfigureForLinuxEvent* event) = 0;
};

struct HayesSmartmodem : public Modem {
  virtual void configure(ConfigureForWindowsEvent* event);
  virtual void configure(ConfigureForLinuxEvent* event);
};

struct USRobotics : public Modem {
  virtual void configure(ConfigureForWindowsEvent* event);
  virtual void configure(ConfigureForLinuxEvent* event);
};

 

Grappig genoeg komen we nu weer op het probleem uit dat C++ geen double dispatch ondersteund (https://en.wikipedia.org/wiki/Dynamic_dispatch#Single_and_multiple_dispatch). Ditmaal gaan we de double dispatch niet op het object (de modem) doen, maar op het event. Daardoor ontstaat precies een gespiegelde oplossing aan het officiële visitor pattern:

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

class ConfigureForWindowsEvent;
class ConfigureForLinuxEvent;

struct Modem {
  virtual void configure(ConfigureForWindowsEvent* event) = 0;
  virtual void configure(ConfigureForLinuxEvent* event) = 0;
};
typedef std::shared_ptr<Modem> ObjectRef;

struct HayesSmartmodem : public Modem {
  virtual void configure(ConfigureForWindowsEvent* event);
  virtual void configure(ConfigureForLinuxEvent* event);
};

struct USRobotics : public Modem {
  virtual void configure(ConfigureForWindowsEvent* event);
  virtual void configure(ConfigureForLinuxEvent* event);
};

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

struct ConfigureForWindowsEvent : public Configurator {
  virtual void configure(ObjectRef& object){object->configure(this);};
};

struct ConfigureForLinuxEvent : public Configurator {
  virtual void configure(ObjectRef& object){object->configure(this);};
};

void HayesSmartmodem::configure(ConfigureForWindowsEvent* event){std::cout << "configure Hayes for Windows\n";}
void HayesSmartmodem::configure(ConfigureForLinuxEvent* event){std::cout << "configure Hayes for Linux\n";}
void USRobotics::configure(ConfigureForWindowsEvent* event){std::cout << "configure USRobotics for Windows\n";}
void USRobotics::configure(ConfigureForLinuxEvent* event){std::cout << "configure USRobotics for Linux\n";}

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

void determineAction(ActionRef& actionRef) {
  actionRef = ActionRef(new ConfigureForWindowsEvent);
}

void applyActionToAllObjects(std::vector<ObjectRef>& objects, ActionRef& actionRef) {
  for (std::vector<ObjectRef>::iterator it = objects.begin(); it != objects.end(); ++it) {
    actionRef->configure(*it);
  }
}

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

  ActionRef actionRef;
  determineAction(actionRef);

  applyActionToAllObjects(objects, actionRef);
  return 0;
}

Deze oplossing is elegant als iedere modem voor elk platform geconfigureerd moet worden. Dit kan tijdens het compileren afgedwongen worden met het interface van Modem.

De overeenkomst met het visitor pattern is het oplossen van het double dispatch probleem, maar in tegengestelde volgorde. Bij het visitor pattern eindigt het trucje buiten de klasse en heeft het tot doel een extern algorithme mogelijk te maken. In dit voorbeeld eindigt het trucje in de klasse en heeft het tot doel het juiste interne algorithme te selecteren. Precies gespiegeld met tegengestelde sterktes en zwaktes, maar met onderliggend hetzelfde trucje.

 

Comments powered by CComment