Visitor Pattern

Visitor Pattern

vg

Das Visitor Pattern ist ein Verhaltensmuster, das es ermöglicht, neue Operationen auf Elementen eines Objekts zu definieren, ohne deren Klassenstruktur zu ändern. Es trennt die Operationen von den Objekten, auf denen sie angewendet werden. Dadurch bleibt der Code flexibel und erweiterbar, da neue Operationen hinzugefügt werden können, ohne bestehende Klassen zu modifizieren.

Was ist das Visitor Pattern?

Das Visitor Pattern besteht aus zwei Hauptkomponenten: dem Visitor und den Elementen. Der Visitor definiert die Operationen, die auf den Elementen ausgeführt werden. Jedes Element hat eine Methode, die den Visitor empfängt und ihm erlaubt, die entsprechende Operation auf sich auszuführen.

Im Wesentlichen erlaubt dieses Muster, dass ein Objekt ein „Besucher“-Objekt empfängt, das dann eine Operation auf diesem Objekt ausführt. Dies macht es einfach, neue Operationen hinzuzufügen, ohne die Klassen der Elemente selbst zu ändern.

Komponenten des Visitor Patterns

  1. Visitor: Ein Interface oder eine abstrakte Klasse, die die Methode visit() für jedes Element definiert. Diese Methode nimmt als Parameter ein Element und führt eine Operation auf diesem Element aus.
  2. ConcreteVisitor: Eine konkrete Implementierung des Visitors. Diese Klasse definiert spezifische Operationen für jedes Element.
  3. Element: Eine abstrakte Klasse oder Schnittstelle, die eine accept()-Methode definiert. Diese Methode nimmt einen Visitor entgegen und lässt diesen die entsprechende Methode ausführen.
  4. ConcreteElement: Eine konkrete Implementierung des Elements, die die accept()-Methode implementiert und den Besuch des Visitors erlaubt.

Beispiel des Visitor Patterns in C++

Im folgenden Beispiel wird das Visitor Pattern verwendet, um eine Operation auf einer Reihe von geometrischen Formen auszuführen, ohne deren Klassen zu ändern.

#include <iostream>
#include <vector>

// Forward declaration der Visitor-Klasse
class ShapeVisitor;

// Abstrakte Element-Klasse
class Shape {
public:
    virtual void accept(ShapeVisitor& visitor) = 0;
    virtual ~Shape() = default;
};

// Konkrete Element-Klassen: Kreis
class Circle : public Shape {
public:
    void accept(ShapeVisitor& visitor) override;
};

// Konkrete Element-Klassen: Rechteck
class Rectangle : public Shape {
public:
    void accept(ShapeVisitor& visitor) override;
};

// Abstrakte Visitor-Klasse
class ShapeVisitor {
public:
    virtual void visit(Circle& circle) = 0;
    virtual void visit(Rectangle& rectangle) = 0;
    virtual ~ShapeVisitor() = default;
};

// Konkreter Visitor: Berechnung des Flächeninhalts
class AreaCalculator : public ShapeVisitor {
public:
    void visit(Circle& circle) override {
        std::cout << "Berechne die Fläche des Kreises" << std::endl;
    }

    void visit(Rectangle& rectangle) override {
        std::cout << "Berechne die Fläche des Rechtecks" << std::endl;
    }
};

// Konkreter Visitor: Berechnung des Umfangs
class PerimeterCalculator : public ShapeVisitor {
public:
    void visit(Circle& circle) override {
        std::cout << "Berechne den Umfang des Kreises" << std::endl;
    }

    void visit(Rectangle& rectangle) override {
        std::cout << "Berechne den Umfang des Rechtecks" << std::endl;
    }
};

// Implementierung der accept-Methode für Circle
void Circle::accept(ShapeVisitor& visitor) {
    visitor.visit(*this);
}

// Implementierung der accept-Methode für Rectangle
void Rectangle::accept(ShapeVisitor& visitor) {
    visitor.visit(*this);
}

// Client-Code
int main() {
    // Erstellen von geometrischen Formen
    Circle circle;
    Rectangle rectangle;

    // Erstellen des Visitors für die Flächenberechnung
    AreaCalculator areaCalculator;

    // Anwenden des Visitors auf die Formen
    circle.accept(areaCalculator);
    rectangle.accept(areaCalculator);

    // Erstellen des Visitors für die Umfangsberechnung
    PerimeterCalculator perimeterCalculator;

    // Anwenden des Umfangs-Berechnungs-Visitors
    circle.accept(perimeterCalculator);
    rectangle.accept(perimeterCalculator);

    return 0;
}

Erklärung des C++-Beispiels

  1. Shape (Abstrakte Element-Klasse): Die Shape-Klasse ist eine abstrakte Basisklasse für alle geometrischen Formen. Sie definiert die Methode accept(), die einen Visitor entgegennimmt und die visit()-Methode des Visitors aufruft.
  2. Circle und Rectangle (Konkrete Element-Klassen): Circle und Rectangle sind konkrete Klassen, die von Shape erben. Sie implementieren die accept()-Methode, die den jeweiligen visit()-Aufruf an den Visitor weitergibt.
  3. ShapeVisitor (Abstrakte Visitor-Klasse): Die ShapeVisitor-Klasse definiert zwei visit()-Methoden, die spezifisch für jedes Element sind. Diese Methoden müssen in den konkreten Visitors implementiert werden.
  4. AreaCalculator und PerimeterCalculator (Konkrete Visitor-Klassen): Diese Klassen implementieren die ShapeVisitor-Schnittstelle und definieren die jeweiligen Operationen. AreaCalculator berechnet die Fläche von Formen, während PerimeterCalculator den Umfang berechnet.
  5. accept(): Die accept()-Methode in den Elementen stellt sicher, dass der passende visit()-Aufruf für das jeweilige Element erfolgt.

Beispiel des Visitor Patterns in Python

Das Visitor-Pattern ist ein Verhaltensmuster, das es ermöglicht, Operationen auf Elementen einer Objektstruktur auszuführen, ohne die Elemente selbst zu verändern. Das Visitor-Pattern trennt den Algorithmus von der Objektstruktur und erlaubt es, neue Operationen hinzuzufügen, ohne die Klassen der Elementobjekte zu ändern.

Hier ist ein einfaches Beispiel für das Visitor-Pattern in Python:

1. Die Element-Klasse

Diese repräsentiert das Element, auf dem der Besucher operieren kann. Alle konkreten Elemente implementieren diese Schnittstelle.

from abc import ABC, abstractmethod

class Element(ABC):
    @abstractmethod
    def accept(self, visitor):
        pass

2. Konkrete Elemente

Nun implementieren wir einige konkrete Element-Klassen, die die accept-Methode definieren, um den entsprechenden Besucher zu empfangen.

class ConcreteElementA(Element):
    def accept(self, visitor):
        visitor.visit_concrete_element_a(self)

    def operation_a(self):
        return "ConcreteElementA operation"

class ConcreteElementB(Element):
    def accept(self, visitor):
        visitor.visit_concrete_element_b(self)

    def operation_b(self):
        return "ConcreteElementB operation"

3. Der Visitor

Das Visitor-Interface definiert die Besuchsmethoden für jedes konkrete Element.

class Visitor(ABC):
    @abstractmethod
    def visit_concrete_element_a(self, element):
        pass

    @abstractmethod
    def visit_concrete_element_b(self, element):
        pass

4. Konkrete Besucher

Nun implementieren wir einige konkrete Besucher, die bestimmte Operationen auf den Elementen durchführen.

class ConcreteVisitor1(Visitor):
    def visit_concrete_element_a(self, element):
        print(f"ConcreteVisitor1: {element.operation_a()}")

    def visit_concrete_element_b(self, element):
        print(f"ConcreteVisitor1: {element.operation_b()}")

class ConcreteVisitor2(Visitor):
    def visit_concrete_element_a(self, element):
        print(f"ConcreteVisitor2: {element.operation_a()}")

    def visit_concrete_element_b(self, element):
        print(f"ConcreteVisitor2: {element.operation_b()}")

5. Anwendung des Visitor Patterns

Jetzt können wir die ConcreteElement-Instanzen mit verschiedenen Visitor-Instanzen besuchen und verschiedene Operationen ausführen.

# Elemente erstellen
elements = [ConcreteElementA(), ConcreteElementB()]

# Besucher erstellen
visitor1 = ConcreteVisitor1()
visitor2 = ConcreteVisitor2()

# Elemente mit verschiedenen Besuchern besuchen
for element in elements:
    element.accept(visitor1)

for element in elements:
    element.accept(visitor2)

Ausgabe:

ConcreteVisitor1: ConcreteElementA operation
ConcreteVisitor1: ConcreteElementB operation
ConcreteVisitor2: ConcreteElementA operation
ConcreteVisitor2: ConcreteElementB operation

Zusammenfassung:

  • Element: Definiert die accept-Methode, die den Besucher empfängt.
  • ConcreteElementA und ConcreteElementB: Implementieren die accept-Methode und erlauben den Besuch von konkreten Besuchern.
  • Visitor: Definiert die Schnittstelle für konkrete Besucher.
  • ConcreteVisitor1 und ConcreteVisitor2: Konkrete Implementierungen von Besuchern, die Operationen auf den Elementen ausführen.

Dieses Beispiel zeigt, wie das Visitor-Pattern es ermöglicht, neue Operationen (durch konkrete Besucher) hinzuzufügen, ohne die bestehenden Elementklassen zu ändern.

Vorteile des Visitor Patterns

  1. Erweiterbarkeit: Neue Operationen können durch das Hinzufügen neuer Visitor-Klassen leicht eingeführt werden. Es ist nicht nötig, die bestehenden Element-Klassen zu ändern.
  2. Zentralisierung der Logik: Die Operationen sind im Visitor zentralisiert, was den Code übersichtlicher macht. Es gibt keine Notwendigkeit, dieselbe Logik in mehreren Element-Klassen zu duplizieren.
  3. Trennung der Verantwortlichkeiten: Das Visitor Pattern trennt die Datenstruktur der Elemente von der Logik, die auf diesen Elementen ausgeführt wird. Dies sorgt für sauberen und wartbaren Code.
  4. Flexibilität: Das Muster ermöglicht es, die Operationen auf den Elementen zu ändern oder zu erweitern, ohne die Struktur der Element-Klassen zu beeinflussen.

Nachteile des Visitor Patterns

  1. Erhöhte Komplexität: Es können viele zusätzliche Klassen und Interfaces erforderlich sein, was den Code unnötig komplex machen kann, wenn nicht viele unterschiedliche Operationen benötigt werden.
  2. Verletzung des Open/Closed-Prinzips: Wenn neue Elemente eingeführt werden, muss der Visitor angepasst werden, um diese neuen Typen zu unterstützen. Dies kann zu Problemen führen, wenn viele verschiedene Element-Typen existieren.
  3. Enge Kopplung zwischen Elementen und Visitors: Das Element kennt den Visitor und ruft dessen Methoden auf. Dies führt zu einer stärkeren Kopplung zwischen den beiden, was in einigen Fällen unerwünscht sein kann.

Wann sollte man das Visitor Pattern einsetzen und wann nicht?

Das Visitor Pattern sollte in bestimmten Situationen eingesetzt werden, wenn die folgenden Bedingungen zutreffen:

1. Trennung von Algorithmus und Objektstruktur

  • Das Visitor Pattern ist besonders nützlich, wenn du eine stabile Objektstruktur hast (z. B. eine Sammlung von verschiedenen Klassen, die keine häufigen Änderungen erfahren) und gleichzeitig unterschiedliche, sich verändernde Operationen auf diese Struktur anwenden möchtest.
  • Du kannst neue Operationen hinzufügen, ohne die bestehenden Klassen zu ändern, da der Algorithmus (der Besucher) von den Elementen getrennt bleibt.

2. Viele verschiedene Operationen auf einer festen Objektstruktur

  • Wenn du eine Gruppe von Objekten hast, auf denen viele unterschiedliche Operationen ausgeführt werden müssen, aber du diese Operationen nicht in den Objekten selbst definieren möchtest (z. B. um eine klare Trennung von Verantwortung zu wahren).
  • Zum Beispiel in einem komplexen Baum- oder Objektmodell, auf dem verschiedene Algorithmen ausgeführt werden (z. B. Analysen, Transformationen, Berechnungen).

3. Vermeidung der Modifikation von existierenden Klassen

  • Das Visitor Pattern ist nützlich, wenn du die bestehende Objektstruktur nicht ändern kannst oder möchtest, aber neue Funktionen hinzufügen musst. Es erlaubt es, neue Operationen zu definieren, ohne die Quellcodebasis der Elementklassen zu verändern.
  • Dies ist besonders hilfreich, wenn du mit schwer wartbarem oder geschlossenem Code arbeitest (z. B. wenn du mit einer externen Bibliothek arbeitest, auf deren Quellcode du keinen Einfluss hast).

4. Komplexe und häufig wechselnde Operationen

  • Wenn die Anzahl der Operationen, die auf die Objekte angewendet werden müssen, groß ist und sich häufig ändern könnte, ohne dass die Objektstruktur geändert wird. Das Visitor Pattern ermöglicht es, neue Operationen zu definieren und so die Änderung des bestehenden Codes zu vermeiden.

5. Wenn du die gleiche Struktur für verschiedene Datenarten verwenden möchtest

  • Es ist besonders nützlich, wenn du eine ähnliche Logik oder Struktur auf viele unterschiedliche Objekte anwenden möchtest, ohne zu viel Code zu wiederholen.

Beispiele für Anwendungsfälle:

  • Compilers und Interpreter: In Compiler-Designs wird das Visitor Pattern verwendet, um verschiedene Analysen und Transformationen auf den abstrakten Syntaxbaum (AST) anzuwenden.
  • Datenmodellierung und -analyse: Wenn du ein System zur Analyse von Finanzdaten, mathematischen Ausdrücken oder anderen komplexen Modellen hast, auf die du viele verschiedene Operationen anwenden möchtest (z. B. Berechnungen, Validierungen, Optimierungen), dann ist das Visitor Pattern sehr hilfreich.
  • Graphen und Baumstrukturen: Wenn du eine komplexe Baumstruktur hast (z. B. einen Dateisystembaum oder einen syntaktischen Baum), auf den verschiedene Traversierungen und Operationen angewendet werden müssen.

Wann sollte man das Visitor Pattern nicht verwenden?

  • Zu geringe Anzahl an Operationen: Wenn du nur wenige Operationen benötigst, kann das Visitor Pattern unnötig komplex sein. Hier wären einfachere Lösungen oft geeigneter.
  • Dynamische Änderungen der Objektstruktur: Wenn die Struktur der Objekte häufig geändert wird (z. B. wenn Klassen und Hierarchien ständig verändert werden), kann das Visitor Pattern unpraktisch sein, da du bei jeder Änderung der Objektstruktur auch den Visitor anpassen musst.
  • Wenn keine Trennung von Operationen und Struktur notwendig ist: Wenn es keinen Bedarf gibt, das Verhalten von der Struktur zu trennen oder das Hinzufügen neuer Operationen ohne Änderung der Objektstruktur zu ermöglichen, dann ist das Visitor Pattern möglicherweise nicht die beste Wahl.

Zusammengefasst: Verwende das Visitor Pattern, wenn du viele Operationen auf einer stabilen Struktur anwenden möchtest und dabei die Trennung von Verantwortung (zwischen Struktur und Operationen) sowie Erweiterbarkeit ohne Veränderung der bestehenden Klassen benötigst.

Fazit

Das Visitor Pattern ist ein nützliches Designmuster, um neue Operationen auf einer Gruppe von Objekten durchzuführen, ohne deren Klassenstruktur zu ändern. Es bietet Flexibilität und Erweiterbarkeit, indem es die Logik vom Objekt trennt. Das Beispiel mit den geometrischen Formen zeigt, wie Besucher-Objekte zur Durchführung spezifischer Operationen auf den Elementen verwendet werden. Allerdings kann das Muster in Szenarien mit vielen unterschiedlichen Elementen oder Operationen zu einer erhöhten Komplexität führen.

Zur Übersicht Design-Pattern: Liste der Design-Pattern

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)