Skip to content

Latest commit

 

History

History
1362 lines (872 loc) · 38.6 KB

9. Design Patterns.md

File metadata and controls

1362 lines (872 loc) · 38.6 KB

9. Design Pattern

Guiding principle: program to interfaces, not implementations.

Abstract Base classes define the interface, work with base class pointers and call their (usually virtual) methods. Concrete subclasses can then be swapped in and out - provides abstraction over a variety of behaviour.

Example: Iterator Pattern

class AbstractIterator { // could make a template
  public:
    virtual int &operator*() const = 0; // could be a template type
    virtual AbstractIterator &operator++() = 0;
    bool operator!=(const AbstractIterator &other) const = 0;
    virtual ~AbstractIterator();
};
class List {
    ...
  public:
    class Iterator : public AbstractIterator {
        ...
    };
    ...
};
class Set {
    ...
  public:
    class Iterator : public AbstractIterator {
        ...
    };
    ...
};

Now we can write functions that operate on Abstract Iterators. In this way, those functions can operate on both Sets and Lists (and any other types whose iterator inherits from Abstract Iterator).

For example:

void forEach(AbsractIterator &start, AbstractIterator &end,
             void (*f) (int)) {
    while (start != end) {
        f (*start);
        ++start;
    }
}

forEach works on List and Set.

Observer Pattern

The Observer design pattern is also knowns as Dependents or Publish-Subscribe.

It is intended to create a one-to-many dependency between objects such that all observers are notified when the state of the subject object being observed changes.

A publish-subscribe model.

One class: publisher/subject - generate data/notification.

One or more subscriber/observer - receive the data and react to it.

Example:

Publisher: spread sheet cells

Observer: graphs - update when cells are changed; cells - formula cells that update when the cells they target are updated.

The General UML:

9-1

The UML: the Subject typically does not need virtual methods.

Subject base class "has-a" Observer base class. Object class has a virtual method: notify(), a concrete observer class will override that method.

A concrete subject will have one or more function to allow it to get the stage. A concrete observer have a concrete subject.

9-1

The abstract Subject contains code common to all subjects. The abstract Observer contains the interface common to all observers.

Sequence of methods calls:

  1. Concrete Subject state is updated
  2. Concrete Subject callsSubject::notifyObservers(),, which calls notify on all (concrete) observers currently attached.
  3. Each (concrete) observer calls getState() (or whatever method that gets the information they need) on their subject(s), and react accordingly.

Notes:

  • Subject and Observer classes are abstract base classes
  • Subject contains the code common to all subclasses that they inherit. There is no reason to make the attach and detach methods virtual, since the code will be the same for all subclasses. There is no need to duplicate the information in the concrete subclasses
  • Subject::notifyObservers is usually declared protected, since it should only be called by the concrete subject objects. In certain cases, it may be made public so that an external source can trigger the notifications.
  • The Subject has an aggregate relationship to the Observer class. Therefore, it is not responsible for destroying them when its destructor is run.
  • Concrete observers may dynamically attach or detach themselves from a concrete subject during the program run-time
  • Every time the concrete subject changes state, all of the concrete observers are notified by calling Subject::notifyObservers, which calls their virtual notify method. (Observer::notify() is pure virtual)
  • The subject and observer classes are loosely coupled since they interact and only need to know that the classes follow the specified interface i.e. thet don't need to know details of the concrete classes other than what is specified by the subject and observer public methods. New observer types only need to inherit from the Observer class. Same for new subject types.

Example: Horse races, subject publishes the winner, observers are individual bettors. They declare victory when their horse wins.

// subject.h

#ifndef _SUBJECT_H_
#define _SUBJECT_H_
#include <vector>
#inlcude "observer.h"

class Subject {
    std::vector<Observer*> observers;	// pointer of observers
  public:
    Subject();
    void attach( Observer *o );
    void detach( Observer *o );
    void notifyObservers();
    virtual ~Subject() = 0;
};

#endif
// subject.cc

#include "subject.h"

Subject::Subject() {}
Subject::~Subject() {}

void Subject::attach( Observer *o ) {
    observers.emplace_back(o);
}

void Subject::detach( Observer *o ) {
    for (auto it = observers.begin(); it != observers.end(); ++it) {
        if (*it == o) {
            observers.erase(it);
            break;
        }
    }
}

void Subject::notifyObservers() {
    for (auto ob : observers) ob->notify();
}
// observer.h

#ifndef _OBSERVER_H_
#define _OBSERVER_H_

class Observer {
  public:
	virtual void notify() = 0;
    virtual ~Observer();
};

#endif
// Observer.cc

#include "observer.h"

Observer::~Observer() {}

The bettor class: concrete observer.

// bettor.h

#ifndef _BETTOR_H_
#define _BETTOR_H_
#include "observer.h"
#include "horserace.h"

class Bettor : public Observer {
    HorseRase *subject;
    const std::string name;
    const std::string myHorse;
    
  public:
    Bettor(HorseRase *hr, std::string name, std::string horse);
    void notify() override;
    ~Bettor();
};

#endif
// bettor.cc

#include <iostream>
#include "bettor.h"

Bettor::Bettor(HorseRace *hr, std::string name, std::string horse)
    : subject{hr}, name{name}, myHorse{horse}
{
    subject->attach( this );
}

Bettor::~Bettr() {
    subject->detach( this );	// Important to detach!
}

void Bettor::notify() {
    std::cout << name
        << (subject->getState() == myHorse ? "wins! Off to collect." : "loses.")
        << std::endl;
}

The horserace class: concrete subject

// horserace.h

#ifndef _HORSERACE_H_
#define _HORSERACE_H_
#include <fstream>
#inlcude <string>
#include "subject.h"

class HorseRace : public Subject {
    std::fstream in;
    std::string lastWinner;
    
  public:
    HorseRace(std::string source);
    ~HorseRace();
    
    bool runRace(); // Returns true if a race was successfuly run.
    
    std::string getState();
};

#endif
// horserace.cc
#include <iostream>
#include "horserace.h"

HorseRace::HorseRace(std::string source) : in{source} {}

HorseRace::~HorseRace() {}

bool HorseRace::runRace() {
    bool result {in >> lastWinner};
    if (result) std::cout << "Winner: " << lastWinner << std::endl;
    // the concrete subject can notify observers
    if (result) notifyObservers();
    return result;
}

std::string HorseRace::getState() {
    return lastWinner;
}

In main, we set up our horserace:

// main.cc
#include <iostream>
#include "bettor.h"

int main(int argc, char **argv) {
    std:;string raceData = "race.txt";
    if (argc > 1) raceData = argv[1];
    
    HorseRace hr{raceData};
    
    Bettor Larry{&hr, "Larry", "RunsLikeACoW"};
    Better Moe{&hr, "Moe", "Molasses"};
    Better Curly{&hr, "Curly", "TurtlePower"};
    
    int count = 0;
    Better *Shemp = nullptr;	// Shemp is going to come in for some of the races, not all
    
    while (hr.runRace()) {
        if (count == 2) Shemp = new Bettor{&hr, "Shemp", "GreasedLightning"};
        if (count == 5) delete Shemp;
        ++count;
    }
    if (count < 5) delete Shemp;
}
  • since "bettor.h" already includes "horserace.h", we don't need to explicitly include it, though we could
  • the concrete subject, hr, is initialized with the race results, i.e. the name of the winner for each race

The information is pulled by the observer. The subject notifies the observer that the event has occurred, and it's up to the observer to retrieve the desired information from the subject.

Decorator Pattern

Suppose that we want to enhance an object, e.g. add or change behaviour at runtime.

The Decorator design pattern in intended to let you add functionality or features to an object at run-time rather than to the class as a whole. These functionalities or features might also be withdrawn. We will end up having lots of little objects that look rather similar to each other, and differ only in how they're connected.

For example: a windowing system (that display windows on your computer), we could start with a basic window, and then add a scrollbar, and then maybe a menu. We want to be able to choose which features to apply to which windows at runtime.

Other examples:

  • Telephones: call waiting, caller ID, call forwarding, distinctive ringing, automatic callback, speed dialing, redial
  • Word processors: track changes, paragraph format, line spacing, highlighting, text font, text colour, sticky notes, spell checking
  • Games: character's appearance, hair, hat, glasses, beard, moustache, jewelry, piercings, tattoos, clothing

The UML: Start with a base class, component, it can have one or more virtual methods. We need two derived classes, one of them is the concrete component of the basic component (override the methods in base class). Then, we have a second derived class, this is also an abstract class, called the decorator. The decorator class also need to have operations, and it has a component (aggregation). It's operation just calls component->operation().

Every decorator has a component, that component may be a concrete component or our basic concrete component, or it may be a concrete decorator.

9-2

  • The decorator base class has (can owns) component (will have a pointer to a component). It can either be decorating a concrete base component, or something that has already be decorated (concreate decorator A & B).
    • This lets us build a linked list of decoration objects by having a pointer to a Component. Since all of the decoration classes inherit from the Component, we don't need wo know what is in the linked list once it's built since the operation we are invoking is virtual (likely pure virtual) in the Component class.
  • Since the ConcreteComponent inherits from Component, the linked list thus terminates with a concrete component object.
  • Each call to the operation in a decoration object ends up invoking the operation in the next component.
    • In this way, we can build up values or actions by delegating the work to the objects of which the decorated object is composed.
  • New functionalities are introduced by adding new subclasses, not modifying existing code, which reduces the chance of introducing errors

The component class defines the interface: the operations that your objects will provide. Concrete component implements the interface, and is our base starting object. The decorator class inherits from component, AND has a component. All concrete decorators inherit from decorator (they are both decorator and component, because a decorator inherits from component). Therefore, every decorator IS-A component, and HAS (or owns) A component.

e.g. A window with a scrollbar is a scrollbar object (which is a type of window) and has a pointer to the underlying plain window. The scrollbar decorates the plain window. A scrollbar with a menu is a menu with a pointer to the underlying scrollbar which itself has a pointer to the underlying plain window. (the order isn't necessarily important: we can have scrollbar -> menu -> plain window, or menu -> scrollbar -> plain window).

Since these all inherit from a base window type, all operations that work on windows work on them.

The UML of window example

9-7

In order to build a linked-list, all of our effects need to have a pointer to the abstract base class type since we don't know what sort of object they will actually point to at run-time.

Since all of our effects need the pointer, and we don't want our concrete component class to have it, since it is the final node in the list. We would prefer to introduce an intermediate abstract base class, Decorator, that:

  • inherits from Component
  • holds the pointer, and
  • still has pure virtual methods for operation()

The effect classes will inherit from the Decorator.

Example: Pizza: customers adding ingredients to it, the cost of pizza depends upon the items ordered.

9-3

Abstract base Component class

// pizza.h
#ifndef _PIZZA_H_
#define _PIZZA_H_
#include <string>

// abstract component
class Pizza {
  public:
    virtual float price() const = 0;
    virtual std::string description() const = 0;
    virtual ~Pizza();	// destructor always need to be virtual    
};

#endif

// pizza.cc
#include "pizza.h"

Pizza::~Pizza() {}

Concrete Component class:

// crustandsauce.h
#ifndef _CRUSTANDSAUCE_H_
#define _CRUSTANDSAUCE_H_
#include "pizza.h"

// concrete component
class CrustAndSause : public Pizza {
    // override the member functions
  public:
    float price() const override;
    std::string description() const override;
};

#endif

// crustandsauce.cc
#include "curstandsauce.h"

float CrustAndSauce::price() const { return 5.99; }

std::string CrustAndSauce::description() const { return "Pizza"; }

9-4

9-4

The Decorator inherits from abstract component:

// decorator.h
#ifndef _DECORATOR_H_
#define _DECORATOR_H_
#include "pizza.h"

class Decorator : public Pizza {
  protected:
    Pizza *component;
  public:
    Decorator(Pizza *component); // need a decorator constructor
    virtual ~Decorator(); // means we are taking ownship of the component, we need to change this to a own-a relationship in UML
};


// decorator.cc
#include "pizza.h"
#include "decorator.h"

Decorator::Decorator(Pizza *component) : component{component} {}

Decorator::~Decorator() { delete component; }	// need to delete component because it is a pointer to a node

The abstract decorator doesn't actually override the price or description, it is the concrete decorators that overrides it. The decorator is still abstract.

Let's write the concrete decorator classes:

// topping.h
#ifndef _TOPPING_H_
#define _TOPPING_H_
#inlcude "pizza.h"
#include "decorator.h"

class Pizza;	// forward declaration here

class Topping : public Decorator {
    std::string theTopping;	// we know what our topping is
    const float thePrice;
  public:
    Topping(std::string topping, Pizza *component);
    float price() const override;
    std::string description() const override;
};

#endif


// topping.cc
#include "topping.h"
#include "pizza.h"

Topping::Topping(std::string topping, Pizza *component)
    : Decorator{component}, theTopping{topping}, thePrice{0.75} {}

float Topping::price() const { return component->price() + thePrice; }
// we might be decorating something that has already been decorated
std::string Topping::description() const { 
    return component->description() + " with " + theTopping;
}

Other concrete decorator classes are similar:

// stuffedcrust.h
#ifndef _STUFFEDCRUST_H_
#define _STUFFEDCRUST_H_
#include "pizza.h"
#include "decorator.h"
#include <string>


class StuffedCrust : public Decorator {
     // a StuffedCrush is also a Pizza because a Decorator is a Pizza
  public:
    StuffedCrust(Pizza *component);
    float price() const override;
    std::string description() const override;
};

#endif


// stuffedcrust.cc
#include "stuffedcrust.h"

StuffedCrust::StuffedCrust(Pizza *component) : Decorator{component} {}

 // let's see the way we "decorate" the price and description

float StuffedCrust::price() const { 
    return component->price() + 2.69; // 2.69 plus the price of whatever the thing we are decorating
}

std::string StuffedCrust::description() const {
    return compoent->description() + " with stuffed crust"; // concatenate the decorated description
};
// dippingsauce.h
#ifndef _DIPPINGSAUCE_H_
#define _DIPPINGSAUCE_H_	
#include "pizza.h"
#include "decorator.h"
#include <string>

class DippingSauce : public Decorator {
    std::string flavour;
  public:
    DippingSauce(std::string flavour, Pizza *component);
    float price() const override;
    std::string description() const override;
};

// dippingsauce.cc
#include "dippingsauce.h"

DippingSauce::DippingSauce(std::string flavour, Pizza *p)
    : Decorator{p}, flavour{flavour} {}

float DippingSauce::price() { return component->price() + 0.30; }

std::string DippingSauce::description() {
    return component->description() + " with " + flavour + " dipping sauce";
}

How do we use them?

// in main.cc
#include <iostream>
#include <string>
#include <iomanip>
#include "pizza.h"
#include "topping.h"
#include "stuffedcrust.h"
#include "dippingsauce.h"
#include "crustandsauce.h"

int main() {
    Pizza *myPizzaOrder[3];
    
    myPizzaOrder[0] = new Topping{"pepperoni",
                                 new Topping {"cheese", new CrustAndSauce}};
    myPizzaOrder[1] = new StuffedCrust{
        new Topping{"cheese", new Topping{"mushrooms", new CrustAndSauce}}};
    myPizzaOrder[2] = new DippingSauce{"garlic",
                                       new Topping{"cheese",
                                                  new Topping {"cheese",
                                                              new Topping {"cheese",
                                                                           new Topping {"cheese",
                                                                                       new CrustAndSauce}}}}};
    float total = 0.0;
    for (int i = 0; i < 3; ++i) {
        std::cout << myPizzaOrder[i]->description()
            << ": $" << std::fixed << std::showpoint << std::setprecision(2)
            << myPizzaOrder[i]->price() << std::endl;
        total += myPizzaOrder[i]->price();
    }
    std::cout << std::endl << "Total cost: $" << std::fixed << std::showpoint 
      << std::setprecision(2) << total << std::endl;
    
    for (int i = 0; i < 3; ++i) delete myPizzaOrder[i];
    
    // or we could do:
    Pizza p1 = new CrustAndSauce;
	p1 = new Topping{"cheese", p1}; // storing p1 ptr in this new Topping, decorate it with the new topping
	p1 = new Topping{"pineapple", p1};
	p1 = new StuffedCrust{p1};

	cout << p1->description() << " costs " << p1->price() << endl;
	delete p1;
    
}
  • note that we first create the object CrustAndSauce, which is a plain pizza. The creation of a decorated pizza is the creation of a singly linked list.

Factory Method Pattern

Factory Method design pattern provides an interface for object creation, but lets the subclasses decide which object to create. It is also known as the Virtual Constructor.

Abstract a way the policy of object creations. We want different creation for different scenarios.

The general UML:

9-12

Notes:

  • Encapsulates the object creation as the part of the design that changes, which is why it's a virtual method in the Creator super class. It may not necessarily be a pure virtual method if it chooses to provide a default implementation to use.
  • Creator superclass encapsulates the operations other than creation that can be applied to a product i.e.anOperation()
  • A valid variation is to add a parameter to the newProduct() method.

Problem: write a video game with 2 kinds of enemies: turtles and bullets. The system randomly sends turtles and bullets, but bullets come more frequently in castle levels.

At an easy/normal level, you want the random creation of enemies to be 70% bullets and 30% turtles.

9-6

Since we never know exactly which enemy comes next, we can't call their constructors directly. Moreover, we want customizable behaviour for the policy of objects (enemy) creation.

We create a virtual method (createEnemy) to do the thing we want to do differently for each type.

So instead, we put a factory method in level that creates enemies.

// level.h

class Enemy;	// forward declaration of Enemy here

class Level {
  public:
    // factory method
    virtual Enemy *createEnemy() = 0;	// pure virtual factory method
    ...
};


// level.cc
#include "level.h"
// implements whatever isn't pure virtual
class NormalLevel : public Level {
  public:
    Enemy *createEnemy() override {
        double x = rand();  // generate a random # between 1.0 and 0.0
        if (x <= 0.7) {
            return new Turtle{...};
        } else {
            return new Bullet{...};
        }
}; // 70% of enemies on normal levels are turtles, 30% of them of bullets
class Castle : public Level {
  public:
    Enemy *createEnemy() override {
        // code for 50% turtles,
        // and 50% bullets
    }
};
// the use of this

Level *curLevel = new NormalLevel;
Enemy *e = curLevel->createEnemy();

We can create the enemies without needing to know anything about how the choice of which enemy to create is made.

The pattern is also known as the virtual constructor pattern.

Template Method Pattern

The Template Method design pattern defines the steps of an algorithm in an operation, but lets the subclasses redefine certain steps though not the overall algorithm's structure.

Used when we want a subclass to override some aspects of method behaviour, but keep other aspects the same.

The general UML:

9-13

Notes:

  • The abstract base class contains the refactored common behaviour for all subclasses to remove duplicate code
  • Subclasses can only override the provided hooks, the virtual methods
  • The virtual methods may provide a default implementation, or be pure virtual
  • The method that contains the algorithm is not virtual, the steps and their order may not be changed!

Example: there are red turtles and there are green turtles. All turtles have a head, a shell, and feet.

class Turtle {
  public:
    void draw() {
        drawHead();
        drawShell();	// must be specicalized: red or green
        drawFeet();
    }
  private:
    void drawHead() {...} // can be implemented in this base class,
    void drawFeet() {...} // because we want them to stay the same
    virtual void drawShell() = 0; // yes, virtual methods can be private
};

Note: it is perfectly legal to have a private virtual method.

Our concrete red and green Turtles:

class RedTurtle : public Turtle {
    void drawShell() override {
        // draw a red shell
    }
};

class GreenTurtle : public Turtle {
    void drawShell() override {
        // draw a green shell
    }
}

Subclasses can't change the way a turtle is drawn, that is, head then shell then feet, but can change the way in which the shell is drawn.

Non-virtual Interface Idiom

Extension: Non-virtual interface (NVI) idiom

A public virtual method is really two things at once.

  • an interface to the client
    • indicates provided behaviour and pre/post conditions; promises class invariants (i.e. what properties must hold or must be true both before and after the method executes for all instances of the class)
  • an interface to the subclasses
    • a place for the subclasses (virtual) to add their own "hook" to inject specialized behaviour

Problem is that it is hard to separate these two ideas if they are wrapped up in the same function declaration.

  • What if you later want to separate the specialized behaviour into two methods? Maybe with some non-customizable steps in between? But also don't want to change the public interface.
  • How can we make sure that all overriding methods conform to our pre and post conditions and class invariants?

The NVI idiom says:

  • all public methods should be non-virtual
  • all virtual methods should be private (or at least protected)
  • Except for one method, obviously, the destructor. (if the dtor is private, then objects of this type can't actually be destroyed). It needs to be virtual so we can always call the right destructor (in base class or in subclasses). So the destructor should be both public and virtual.

Example: Digital media

// not NVI
class DigitalMedia {
  public:
    virtual void play() = 0;
   	virtual ~DigitalMedia();
};

// Translate to NVI
class DigitalMedia {
  public:
    // no longer have a virtual method, all public methods non virtual
    void play() { doPlay(); }
    virtual ~DigitalMedia();	// dtor need to be public and virtual
  private:
    virtual void doPlay() = 0;
};

Now, if we need to exert extra control over the play() method, we can do it, without changing the interface.

  • we're getting sued for not checking for DRM(media licensing), so we need all of our derived classes to do so.

    We can just rewrite play() (doPlay() cannot be changed)

    void play {
        checkLicense();
        doPlay();
    }
  • We could add more virtual hooks if we want, for example: showCoverArt (album cover for music, DVD cover, etc.)

    public:
      void play() {
      	checkLicense();
      	showCoverArt();
      }
    private:
      virtual void showCoverArt() = 0;
      void checkLicense() {	// this is non-virtual
          ...
          throw BadDRM();
      }
  • All of this without changing the public interface

It's much easier to take this kind of control over our virtual methods from the beginning by starting with the NVI idiom, rather than trying to take control back later (because we have to change all the derived classes).

The NVI idiom extends the Template Method Pattern buy putting EVERY virtual method inside a non-virtual wrapper. There is essentially no disadvantages for following the NVI. As a good compiler will optimize out the extra function call. (and there is a huge upside)

Visitor Pattern (March 22)

The Visitor design pattern allows the programmer to apply an operation to be performed upon the elements contained in an object structure. New

For implementing double dispatch, virtual methods are chosen based on the actual run-time type of the object they are called on. What if you want to choose a method based on two objects?

The General UML Model:

9-9

Motivating Example: Striking enemies with weapon:

What weapon we have available will depend upon what has happened so far in the game, and the various types of weapons available will have varying effects on the enemies. i.e. some weapons may be more effective acting upon certain enemies than others.

9-8

We want something like:

virtual void (Enemy, Weapon)::strike();
// or
virtual void strike(Enemy &e, Weapon &w);

If we put the method in Enemy: virtual void Enemy::string(Weapon &w);, we choose the method based on Enemy, but not weapon.

If we put the method in Weapon, vice versa. This is called dynamic dispatch, and is implemented in C++ as single dispatch. i.e., can be only performed upon one object by looking up its actual type at run-time.

We need a technique called double dispatch, which requires us to combine both overloading and overriding (single dispatch).

We pick Enemy class to apply overriding, we then apply overloading to the Weapon class.

// enemy.h
class Enemy {
  public:
    // override i.e. dynamic dispatch
    virtual void beStruckBy(Weapon &w) = 0;
};

The revised class model is:

9-10

// turtle.h
class Turtle : public Enemy {
  public:
    void beStruckBy(Weapon &w) override {
        w.strike(*this);	
        // *this is a Turtle
    }
};


// bullet.h
class Bullet : public Enemy {
  public:
    void beStruckBy(Weapon &w) {
        w.strike(*this);
        // *this is a Bullet
    }
}

Now we use overloading to get the behaviour we want:

// weapon.h
class Weapon {
  public:
    // overload for other class hierarchy
    virtual void strike(Turtle &t) = 0;
    virtual void strike(Bullet &b) = 0;
};
// stick.h
class Stick : public Weapon {
  public:
    void strike(Turtle &t) {
        // code to hit a turtle with a stick
    }
    void strike(Bullet &b) {
        // code to hit a bullet with a stick
    }
      
};

// rock.h
class Rock : public Weapon {
  public:
    void strike(Turtle &t) {
        // code to hit a turtle with a rock
    }
    void strike(Bullet &b) {
        // code to hit a bullet with a rock
    }
      
};

The client code:

Enemy *e = new Bullet{...};		// create a bullet
Weapon *w = new Rock{...};		// create a rock

e->beStruckBy( *w );

What happens (if we call e->beStruckBy( *w )) ?

  • first, the program has to determine the actual object type for e

  • since e is pointing to a Bullet object, it ends up invoking Bullet::beStruckBy() (virtual dispatch), passing in *w as the parameter called w

  • Once again, we have to determine what the actual object type that w is bound to, which is a rock. (Rock has overloaded the strike method), it c alls Weapon::strike() on *this, since it is in the bullet method, *this is a Bullet

  • therefore the Bullet version run at compile time

  • the virtual method strike(Bullet &b) resolves to Rock::strike(Bullet &b)

This ability to perform double dispatch is at the core of the Visitor design pattern.

Code example:

Visitor can be used to add functionality to existing classes in a hierarchy without changing or recompiling those classes themselves - a class can support "Houns" for visitors. e.g. Books:

Add a BookVisitor class and a concrete Catalogue class to the hierarchy that can be used to traverse the collection and perform the following actions:

  • count the number of distinct book authors
  • count the number of distinct text topics, and
  • count the number of distinct comic heroes

9-11

// BookVisitor.h

#ifndef _BOOKVISITOR_H_
#define _BOOKVISITOR_H_

class Book; // forward declarations
class Text;
class Comic;


class BookVisitor {	// weapon
  public:
    virtual void visit(Book &b) = 0;	// strike
    virtual void visit(Text &t) = 0;
    virtual void visit(Comic &c) = 0;
    virtual ~BookVisitor();
};

#endif
// BookVisitor.cc

#include "BookVisitor.h"
#include "book.h"
#inlcude "text.h"
#include "comic.h"

BookVisitor::~BookVisitor() {}
// catalogue.h

#ifndef _CATALOGUE_H_
#define _CATALOGUE_H_
#include <map>
#include <string>

#include "BookVisitor.h"
class CatalogueVisitor : public BookVisitor {
    std::map<std::string, int> theCatalogue;
  
  public:
    std::map<std::string, int> getResult() const;
    virtual void visit(Book &b) override;
    virtual void visit(Text &t) override;
    virtual void visit(Comic &c) override;
};

#endif
// catalogue.h

#include "catalogue.h"
#include "book.h"
#include "text.h"
#include "comic.h"
using namespace std;

map<string, int> CatalogueVisitor::getResult() const { return theCatalogue; }
void CatalogueVisitor::visit(Book &b) { ++theCatalogue[b.getAuthor()]; }
void CatalogueVisitor::visit(Text &t) { ++theCatalogue[t.getTopic()]; }
void CatalogueVisitor::visit(Comic &c) { ++theCatalogue[c.getHero()]; }
class Book {	// Enemy
  public:
    ...
    virtual void accept(BookVisitor &v) { v.visit(*this); }
};
class Text : public Book {
  public:
    ...
    void accept(BookVisitor &v) { v.visit(*this); }
};
// comic is the same
class Comic : public Book {
  public:
    ...
    void accept(BookVisitor &v) { v.visit(*this); }
}

Application: Track how many of each book type we have. Group Books by Author, Texts by topic, Comics by hero.

We use a map<string, int>, we could add to the Book hierarchy a virtual void updateMap(map<string, int> &m); :

for (Book &b : myBooks) { b.updateMap(myMap); }

Our main program consists of:

// main.cc

#include <string>
#include <vector>
#include "book.h"
#include "text.h"
#include "comic.h"
#include "catalogue.h"

using namespace std;

int main() {
    std::vector<Book*> collection {
    	new Book{ "War and Peace", "Tolstoy", 5000 },
   		new Book{ "Peter Rabbit", "Potter", 50 },
    	new Text{ "Programming for Beginners", "??", 200, "BASIC" },
    	new Text{ "Programming for Big Kids", "??", 200, "C++" },
    	new Text{ "Annotated Reference Manual", "??", 200, "C++" },
    	new Comic{ "Aquaman Swims Again", "??", 20, "Aquaman" },
    	new Comic{ "Clark Kent Loses His Glasses", "??", 
       		20, "Superman" },
    	new Comic{ "Superman Saves the Day", "??", 20, "Superman" }
  	};
    
    CatalogueVisitor v;
    
    for (auto &b : collection) b->accept(v);
    
    for (auto &i : v.theCatalogue)
        cout << i.first << " " << i.second << endl;
    
    for (auto &b : collection) delete b;
}
  • The program creates a vector of Book pointers that is initialized with a set of Book, Text, and Comic objects allocated on the heap
  • A CatalogueVisitor object, v, is created
  • Iterate over the book collection, asking each element to accept the concrete visitor object v, Note that we are using Iterator to iterate over the STL vector and map containers
    • v adds 1 to the author/hero/topic count if it is being accepted by a book/comic/text object
    • If the author/hero/topic didn't previously exist, the map element is created and the count is set to 1
  • We then iterate through the map in v, printing out the information it is indexed by (author/hero/topic) and the associated count
  • We then free the heap allocated memory

Book.h include BookVisitor.h, which includes Text.h, which includes Book.h - a circular include dependency.

// book.h
#include "BookVisitor.h"



// BookVisitor.h
#include "book.h"
#include "text.h"
#include "comic.h"


// text.h
#include "book.h"
#include "BookVisitor.h"

Because of the include guard, text.h doesn't actually get a copy of book.h included, so the compiler doesn't know what book is when we try to inherit from it in text.

But are all of these includes really necessary?

Compilation dependencies

When does a compilation dependency exist, i.e. when does one file really need to include another one?

Consider:

class A {...}; // a.h

class B : public A {
    ...
}; // b.h

class C : {
    A myA;
    ...
}; // c.h

class D {
    A * myApyr;
    ...
}; // d.h

class E {
    A f(A x);
    ...
}; // e.h

class F {
    A f(A x) {... x.someMethod() ...}
    ...
}; // f.h

Which of these require an #include "a.h" and which only need a forward declaration class A;

B: need to include a.h

  • is a subclass inherit from A
  • B has a bigger size than A, the size of B object is dependent on A object

C : need to include a.h

  • size of C object depend on A object

D : forward declaration

  • a pointer, so we don't need to know about the size

E : forward declaration

  • a function that takes an A and returns an A

  • we don't need to know the size of A,

  • we don't need to know any details of A

F : need to include a.h

  • we need to know x.someFunction, and we can only know this by knowing the function of A
  • we need to know the implementation of A, so we need to include a.h

If we need to know the details of a class (size, functions), we need to include.

If we only need to know the things, so we can talk about it, then we only need a forward declaration

Classes B and C have true compilation dependencies, the compiler must know the size. It need to know how big a A is in order to know how big B or C are. Also, B needs to know about the methods (interface).

Classes D and E do not have a true compilation dependency. A forward declaration will suffice. All pointers are the same size, and function prototypes (declaration of a function) are only used for type-checking purposes (since they are not implemented yet). However, we can get ourselves into linking problems for class D.

Class F need to know information about A because it is actually implementing a function. Namely, it needs to at least know that A::someMethod() exists.

If there is no true compilation dependencies necessitated by the code, don't introduce one by having unnecessary #includes.

Additionally, if class A changes, only classes A, B, C, and F need recompilation.

In the implementations files (.cc) of class D and E, the dependency will be stronger.

// d.cc
#include "a.h"

void D::f() {
    MyAptr->someMethod(); // need to know about class A here, so a true dependency exists.
}

When possible, forward declare in the header, and include in the implementation as necessary.