zurück zum Artikel

Patterns in der Softwareentwicklung: Das Besuchermuster

Rainer Grimm
Handschlag

(Bild: gemeinfrei)

Das Besuchermuster kapselt eine auf einer Objekthierarchie ausgefĂŒhrte Operation als Objekt und erlaubt, einfach neue Operation hinzuzufĂŒgen.

Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das Besuchermuster aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software [1]" ist aus zwei GrĂŒnden legendĂ€r. Erstens, weil es so kompliziert ist, und zweitens wegen einer Technik namens Double Dispatch.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tÀtig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und hÀufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschÀftigt er sich intensiv mit seiner Leidenschaft C++.

Double Dispatch beschreibt den Prozess der Auswahl der Mitgliedsfunktion auf der Grundlage des Objekts und der Funktionsargumente. Dass das Besuchermuster so kompliziert ist, liegt natĂŒrlich vor allem daran, dass Double Dispatch in C++ nicht von Haus aus unterstĂŒtzt wird, so wie in Eiffel [2].

Bevor ich auf Single Dispatch und Double Dispatch eingehe, möchte ich zunÀchst auf das Besuchermuster eingehen.

Zweck

Anwendungsfall

Struktur

Visitor

ConcreteVisitor

Element

ConcreteElement

Ich nehme an, diese Darstellung war zu einfach. Das folgende Bild aus dem Besuchermuster [3]auf Wikipedia stellt die Objekthierarchie und die Operationshierarchie gut dar.

UML-Diagramm des Besuchermusters

(Bild: CC BY-SA 3.0 [4])

Dieses Bild vermittelt einen sehr guten Eindruck vom Besuchermuster. Ich möchte es daher nutzen, um die Struktur und das dynamische Verhalten des Besuchermusters zu erklÀren.

Das Besuchermuster besitzt zwei Typenhierarchien. Die Objekthierarchie (CarElement) und die Operationshierarchie (CarElementVisitor). Die Objekthierarchie ist stabil, die Operationshierarchie soll hingegen neue Operationen unterstĂŒtzen. Beide Klassen, CarElement und CarElementVisitor, fungieren als Schnittstellen. Das bedeutet, dass jedes konkrete Autoelement Wheel, Engine, Body und Car die Memberfunktion accept(CarElementVisitor) implementieren muss. Entsprechend muss jede konkrete Operation CarElementDoVisitor und CarElementPrintVisitor die vier Überladungen visit(Wheel), visit(Engine), visit(Body) und visit(Car) implementieren.

Gehen wir davon aus, dass die Operation CarElementPrintVisitor auf die Objekthierarchie angewendet wird. Die Aufgabe von CarElementPrintVisitor könnte sein, den Namen des besuchten Autoteils zu drucken. ZunĂ€chst akzeptiert ein Autoelement wie Motor den Besucher (accept(CarElementVisitor)) und verwendet den Besucher, um mit sich selbst als Argument in die Operationshierarchie zurĂŒckzurufen (visitor.visit(this)). Dadurch wird sichergestellt, dass die visit(Engine)-Überladung des CarElementPrintVisitor aufgerufen wird. Der Besuch eines Autos ist etwas Besonderes. Ein Auto besteht aus verschiedenen Autobestandteilen. Folglich delegiert die accept-Mitgliedsfunktion von car den accept-Aufruf an alle seine Autoteile.

Dies ist die entscheidende Beobachtung des Besuchers. Es hĂ€ngt von zwei Objekten ab, welche Operation durchgefĂŒhrt wird: dem Besucher und dem besuchten Objekt.

Das folgende Beispiel ĂŒbersetzt das vorherige Bild direkt in Code.

// visitor.cpp

#include <iostream>
#include <string>
#include <vector>

class CarElementVisitor;

class CarElement {                                     // (5)
 public:
    virtual void accept(CarElementVisitor& visitor) const = 0;
    virtual ~CarElement() = default;
};

class Body;
class Car;
class Engine;
class Wheel;

class CarElementVisitor {                              // (6)
 public:
    virtual void visit(Body body) const = 0;
    virtual void visit(Car car) const = 0;
    virtual void visit(Engine engine) const = 0;
    virtual void visit(Wheel wheel) const = 0;
    virtual ~CarElementVisitor() = default;
};

class Wheel: public CarElement {
 public:
    Wheel(const std::string& n): name(n) { }

    void accept(CarElementVisitor& visitor) const override {
        visitor.visit(*this);
    }

    std::string getName() const {
        return name;
    }
 private:
    std::string name;
};

class Body: public CarElement {
 public:
    void accept(CarElementVisitor& visitor) const override {
        visitor.visit(*this);
    }
};

class Engine: public CarElement {
 public:
    void accept(CarElementVisitor& visitor) const override {
        visitor.visit(*this);
    }
};

class Car: public CarElement {
 public:
    Car(std::initializer_list<CarElement*> carElements ):
      elements{carElements} {}
   
    void accept(CarElementVisitor& visitor) const override {
        for (auto elem : elements) {
            elem->accept(visitor);
        }
        visitor.visit(*this);
    }
 private:
    std::vector<CarElement*> elements;                   // (7)
};

class CarElementDoVisitor: public CarElementVisitor {
   
    void visit(Body body) const override {
        std::cout << "Moving my body" << '\n';
    }

     void visit(Car car) const override {
        std::cout << "Starting my car" << '\n';
    }

    void visit(Wheel wheel) const override {
        std::cout << "Kicking my " << wheel.getName() 
          << " wheel" << '\n';
    }

    void visit(Engine engine) const override {
        std::cout << "Starting my engine" << '\n';
    }
};

class CarElementPrintVisitor: public CarElementVisitor {
   
    void visit(Body body) const override {
        std::cout << "Visiting body" << '\n';
    }

     void visit(Car car) const override {
        std::cout << "Visiting car" << '\n';
    }

    void visit(Wheel wheel) const override {
        std::cout << "Visiting " << wheel.getName() 
          << " wheel" << '\n';
    }

    void visit(Engine engine) const override {
        std::cout << "Visiting engine" << '\n';
    }
};

int main() {

    std::cout << '\n';

    Wheel wheelFrontLeft("front left");        
    Wheel wheelFrontRight("front right");
    Wheel wheelBackLeft("back left");
    Wheel wheelBackRight("back right");
    Body body;
    Engine engine;
    Car car {&wheelFrontLeft, &wheelFrontRight, 
             &wheelBackLeft, &wheelBackRight,
             &body, &engine};

    CarElementPrintVisitor carElementPrintVisitor; 

    engine.accept(carElementPrintVisitor);       // (1)      
    car.accept(carElementPrintVisitor);          // (2)

    std::cout << '\n';

    CarElementDoVisitor carElementDoVisitor;

    engine.accept(carElementDoVisitor);          // (3)
    car.accept(carElementDoVisitor);             // (4)

    std::cout << '\n';

}

Zu Beginn der main-Funktion werden alle Bestandteile des Autos erstellt. Danach nehmen die engine und das car die carElementPrintVisitor an (1 und 2). In den Zeilen (3) und (4) werden beide Objekte von carElementDoVisitor angenommen. CarElement (5) und CarElementVisitor (6) sind die abstrakten Basisklassen der Objekthierarchie und der Operationshierarchie. Entsprechend der Abbildung werden die konkreten Autoelemente und Besucher erstellt. Das Auto ist die interessanteste Komponente, denn es hÀlt seine Komponenten in einem std::vector<Element*> (7).

Hier ist die Ausgabe des Programms:

Verwandte Muster

Das Besuchermuster ist wohl das Entwurfsmuster mit der höchsten Musterdichte aus dem Buch "Design Patterns: Elements of Reusable Object-Oriented Software [5]".

Vorteile

Nachteile

Der Grund fĂŒr die Kompliziertheit des Besuchermusters ist vor allem der Double Dispatch.

Bevor ich auf Double Dispatch eingehe, möchte ich mich mit Single Dispatch oder virtuellen Funktionsaufrufen beschÀftigen.

Single Dispatch

Beim Single Dispatch entscheidet das Objekt, welche Mitgliedsfunktion aufgerufen wird. Um VirtualitÀt in C++ zu erreichen, braucht man zwei Zutaten: eine Indirektion wie ein Zeiger oder eine Referenz und eine virtuelle Mitgliedsfunktion.

// singleDispatch.cpp

#include <iostream>

class Ball {
 public:
    virtual std::string getName() const = 0;
    virtual ~Ball() = default;
};

class HandBall: public Ball {
    std::string getName() const override {
        return "HandBall";
    }
};

int main() {

    std::cout << '\n';

    HandBall hBall;

    Ball* ballPointer = &hBall;                         // (1)
    std::cout << "ballPointer->getName(): " 
      << ballPointer->getName() << '\n';

    Ball& ballReference = hBall;                        // (2)
    std::cout << "ballReference.getName(): " 
      << ballReference.getName() << '\n';

    std::cout << '\n';

}

Der Ausdruck Ball* ballPointer = &hBall (1) hat zwei Typen. Den statischen Typ (Ball*) und den dynamischen Typ (Handball*), der durch die Adresse des Operators & zurĂŒckgegeben wird. Aufgrund der VirtualitĂ€t der Mitgliedsfunktion getName wird der Aufruf der Mitgliedsfunktion zur Laufzeit bestimmt. Folglich findet ein dynamischer Aufruf statt und die Mitgliedsfunktion getName wird aufgerufen. Eine Ă€hnliche Argumentation gilt fĂŒr die in (2) verwendete Referenz.

Dies ist die Ausgabe des Programms:

Analysieren wir nun den Double Dispatch, der im Besuchermuster verwendet wird.

Beim Double Dispatch hĂ€ngt es von zwei Objekten ab, welche Operation ausgefĂŒhrt wird.

Das bedeutet im konkreten Fall des Programms visitor.cpp, dass der Besucher und das besuchte Objekt es gemeinsam bestimmen, welche Mitgliedsfunktion aufgerufen wird.

Um es konkret zu machen: Welche Aktionen werden durch den Aufruf car.accept(carElementDoVisitor) (4) gestartet? Der Einfachheit halber ist hier die Mitgliedsfunktion accept.

void accept(CarElementVisitor& visitor) const override {
    for (auto elem : elements) {
        elem->accept(visitor);
    }
    visitor.visit(*this);
}

Die Mitgliedsfunktion accept von car durchlĂ€uft alle Elemente und ruft elem->accept(visitor) fĂŒr diesen auf: elem ist ein Zeiger und accept eine virtuelle Funktion => dynamischer Dispatch

Schließlich ruft der Besucher visit auf sich selbst auf, wobei er das besuchte Element als Argument verwendet: visitor.visit(*this).Folglich wird die entsprechende Überladung des Besuchers aufgerufen => static Dispatch

Double Dispatch ist im Fall des Visitor ein Ping-Pong-Spiel zwischen dem Element (Auto-Element) und dem Visitor. Das Auto-Element wendet einen dynamischen Dispatch (override) an und der Visitor einen statischen Dispatch (overload).

Die Template-Methode [10] ist ein verhaltensbasiertes Entwurfsmuster, das ein Template fĂŒr einen Algorithmus definiert. In C++ verwenden wir gerne eine spezielle Variante: Non-Virtual Interface (NVI) [11]. Ich werde die Template-Methode in meinem nĂ€chsten Artikel genauer vorstellen. (rme [12])


URL dieses Artikels:
https://www.heise.de/-7357177

Links in diesem Artikel:
[1] https://en.wikipedia.org/wiki/Design_Patterns
[2] https://en.wikipedia.org/wiki/Eiffel_(programming_language)
[3] https://en.wikipedia.org/wiki/Visitor_pattern
[4] https://commons.wikimedia.org/w/index.php?curid=122709059
[5] https://en.wikipedia.org/wiki/Design_Patterns
[6] https://www.heise.de/blog/Patterns-in-der-Softwareentwicklung-Das-Kompositum-Muster-7323502.html
[7] https://en.wikipedia.org/wiki/Iterator_pattern
[8] https://www.heise.de/blog/Softwareentwicklung-Das-Design-Pattern-Fabrikmethode-zum-Erzeugen-von-Objekten-7252845.html
[9] https://en.wikipedia.org/wiki/Prototype_pattern
[10] https://en.wikipedia.org/wiki/Template_method_pattern
[11] https://en.wikipedia.org/wiki/Non-virtual_interface_pattern
[12] mailto:rme@ix.de