345 Stimmen

Wann sollte ich das Visitor Design Pattern verwenden?

Ich sehe immer wieder Hinweise auf das Besuchermuster in Blogs, aber ich muss zugeben, dass ich es nicht verstehe. Ich lese die wikipedia Artikel für das Muster und ich verstehe seine Funktionsweise, aber ich bin immer noch verwirrt, wann ich es verwenden würde.

Als jemand, der erst kürzlich wirklich hat das Dekorationsmuster und sieht nun überall Anwendungen dafür. Ich möchte dieses scheinbar praktische Muster auch wirklich intuitiv verstehen können.

6voto

Fuhrmanator Punkte 9549

Cay Horstmann hat ein gutes Beispiel dafür, wo man sich bewerben kann Besucher in seinem Buch OO Design and patterns . Er fasst das Problem zusammen:

Zusammengesetzte Objekte haben oft eine komplexe Struktur, die aus einzelnen Elementen besteht. Einige Elemente können wiederum untergeordnete Elemente haben. ... Eine Operation auf einem Element besucht seine untergeordneten Elemente, wendet die Operation auf sie an und kombiniert die Ergebnisse. ... Es ist jedoch nicht einfach, neue Operationen zu einem solchen Entwurf hinzuzufügen.

Der Grund dafür, dass dies nicht so einfach ist, liegt darin, dass die Operationen innerhalb der Strukturklassen selbst hinzugefügt werden. Stellen Sie sich zum Beispiel vor, Sie haben ein Dateisystem:

FileSystem class diagram

Hier sind einige Operationen (Funktionalitäten), die wir mit dieser Struktur implementieren möchten:

  • Anzeige der Namen der Knotenelemente (eine Dateiliste)
  • Anzeige der berechneten Größe der Knotenelemente (wobei die Größe eines Verzeichnisses die Größe aller seiner untergeordneten Elemente umfasst)
  • usw.

Man könnte jeder Klasse im FileSystem Funktionen hinzufügen, um die Operationen zu implementieren (was in der Vergangenheit auch schon gemacht wurde, da es sehr offensichtlich ist, wie man es macht). Das Problem ist, dass jedes Mal, wenn Sie eine neue Funktionalität hinzufügen (die obige Zeile "usw."), Sie möglicherweise immer mehr Methoden zu den Strukturklassen hinzufügen müssen. Irgendwann, nach einer gewissen Anzahl von Operationen, die Sie Ihrer Software hinzugefügt haben, machen die Methoden in diesen Klassen im Hinblick auf den funktionalen Zusammenhalt der Klassen keinen Sinn mehr. Sie haben zum Beispiel eine FileNode die eine Methode hat calculateFileColorForFunctionABC() um die neuesten Visualisierungsfunktionen im Dateisystem zu implementieren.

Das Visitor Pattern (wie viele andere Entwurfsmuster) entstand aus der Schmerzen und Leiden von Entwicklern, die wussten, dass es eine bessere Möglichkeit gab, ihren Code zu ändern, ohne dass überall viele Änderungen erforderlich waren, und die außerdem gute Entwurfsprinzipien (hohe Kohäsion, geringe Kopplung) einhielten. Ich bin der Meinung, dass es schwer ist, den Nutzen vieler Muster zu verstehen, bevor man diesen Schmerz nicht selbst erlebt hat. Den Schmerz zu erklären (wie wir es oben mit den hinzugefügten "usw."-Funktionalitäten versuchen) nimmt Platz in der Erklärung ein und lenkt ab. Aus diesem Grund ist es schwer, Muster zu verstehen.

Visitor ermöglicht es uns, die Funktionalitäten der Datenstruktur zu entkoppeln (z.B., FileSystemNodes ) aus den Datenstrukturen selbst. Das Muster erlaubt es dem Design, Kohäsion zu respektieren - Datenstrukturklassen sind einfacher (sie haben weniger Methoden) und auch die Funktionalitäten sind gekapselt in Visitor Implementierungen. Dies geschieht über Doppeldisposition (das ist der komplizierte Teil des Musters): mit accept() Methoden in den Strukturklassen und visitX() Methoden in den Visitor-Klassen (der Funktionalität):

FileSystem class diagram with Visitor applied

Diese Struktur ermöglicht es uns, neue Funktionalitäten hinzuzufügen, die auf der Struktur als konkrete Besucher arbeiten (ohne die Strukturklassen zu ändern).

FileSystem class diagram with Visitor applied

Zum Beispiel, ein PrintNameVisitor das die Verzeichnislistenfunktionalität implementiert, und eine PrintSizeVisitor die die Version mit der Größe implementiert. Wir könnten uns vorstellen, eines Tages einen "ExportXMLVisitor" zu haben, der die Daten in XML generiert, oder einen anderen Besucher, der sie in JSON generiert, usw. Wir könnten sogar einen Besucher haben, der meinen Verzeichnisbaum anzeigt, indem er ein grafische Sprache wie DOT , die mit einem anderen Programm visualisiert werden sollen.

Ein letzter Hinweis: Die Komplexität von Visitor mit seinem Double-Dispatch bedeutet, dass es schwieriger zu verstehen, zu programmieren und zu debuggen ist. Kurz gesagt, es hat einen hohen Geek-Faktor und widerspricht dem KISS-Prinzip. In einer von Forschern durchgeführten Umfrage wurde festgestellt, dass das Muster "Visitor" umstritten ist (es gab keinen Konsens über seine Nützlichkeit). Einige Experimente zeigten sogar, dass der Code dadurch nicht leichter zu pflegen ist.

5voto

mixturez Punkte 121

Visitor Pattern als die gleiche unterirdische Implementierung zur Aspect Object Programmierung.

Wenn Sie zum Beispiel eine neue Operation definieren, ohne die Klassen der Elemente zu ändern, auf die sie wirkt

5voto

kaosad Punkte 1173

Meiner Meinung nach ist der Arbeitsaufwand für das Hinzufügen eines neuen Vorgangs mehr oder weniger derselbe, wenn man Visitor Pattern oder direkte Änderung der Struktur der einzelnen Elemente. Auch wenn ich eine neue Elementklasse hinzufügen würde, sagen wir Cow ist die Schnittstelle Operation betroffen, was sich auf alle bestehenden Elementklassen auswirkt und somit eine Neukompilierung aller Elementklassen erforderlich macht. Was ist also der Sinn?

5voto

Access Denied Punkte 7805

Ich habe dieses Muster nicht verstanden, bis ich mit Onkel-Bob-Artikel und lesen Sie die Kommentare. Betrachten Sie den folgenden Code:

public class Employee
{
}

public class SalariedEmployee : Employee
{
}

public class HourlyEmployee : Employee
{
}

public class QtdHoursAndPayReport
{
    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        foreach (Employee e in employees)
        {
            if (e is HourlyEmployee he)
                PrintReportLine(he);
            if (e is SalariedEmployee se)
                PrintReportLine(se);
        }
    }

    public void PrintReportLine(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hours");
    }
    public void PrintReportLine(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    }
}

class Program
{
    static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }
}

Es mag zwar gut aussehen, da es bestätigt, dass Einzelne Verantwortung es verstößt Offen/Geschlossen Prinzip. Jedes Mal, wenn Sie einen neuen Mitarbeitertyp haben, müssen Sie "if" mit der Typenprüfung hinzufügen. Und wenn Sie das nicht tun, werden Sie das zur Kompilierzeit nie wissen.

Mit dem Visitor-Pattern können Sie Ihren Code sauberer gestalten, da es nicht gegen das Open/Closed-Prinzip verstößt und die Einzelverantwortung nicht verletzt. Und wenn Sie vergessen, visit zu implementieren, wird er nicht kompiliert:

public abstract class Employee
{
    public abstract void Accept(EmployeeVisitor v);
}

public class SalariedEmployee : Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public class HourlyEmployee:Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public interface EmployeeVisitor
{
    void Visit(HourlyEmployee he);
    void Visit(SalariedEmployee se);
}

public class QtdHoursAndPayReport : EmployeeVisitor
{
    public void Visit(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hourly");
        // generate the line of the report.
    }
    public void Visit(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    } // do nothing

    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        QtdHoursAndPayReport v = new QtdHoursAndPayReport();
        foreach (var emp in employees)
        {
            emp.Accept(v);
        }
    }
}

class Program
{

    public static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }       
}  
}

Der Zauber besteht darin, dass während v.Visit(this) gleich aussieht, ist es in Wirklichkeit anders, da es verschiedene Überladungen von visitor aufruft.

3voto

Ravindra babu Punkte 45577

Besucher

Visitor ermöglicht es, einer Familie von Klassen neue virtuelle Funktionen hinzuzufügen, ohne die Klassen selbst zu verändern; stattdessen wird eine Visitor-Klasse erstellt, die alle entsprechenden Spezialisierungen der virtuellen Funktion implementiert

Besucherstruktur:

enter image description here

Verwenden Sie das Besuchermuster, wenn:

  1. Ähnliche Operationen müssen durchgeführt werden über Objekte verschiedener Typen, die in einer Struktur gruppiert sind
  2. Sie müssen viele verschiedene und nicht miteinander verbundene Vorgänge ausführen. Es trennt Operation von Objekten Struktur
  3. Neue Operationen müssen ohne Änderung der Objektstruktur hinzugefügt werden
  4. Zusammengehörige Vorgänge in einer einzigen Klasse zusammenfassen anstatt Sie zu zwingen, Klassen zu ändern oder abzuleiten
  5. Fügen Sie Funktionen zu Klassenbibliotheken hinzu, für die Sie entweder nicht über die Quelle verfügen oder die Quelle nicht ändern können

Auch wenn Besucher Muster bietet die Flexibilität, neue Operationen hinzuzufügen, ohne den bestehenden Code in Object zu ändern, aber diese Flexibilität hat auch einen Nachteil.

Wenn ein neues Visitable-Objekt hinzugefügt wurde, sind Codeänderungen in den Klassen Visitor und ConcreteVisitor erforderlich . Es gibt eine Abhilfe für dieses Problem: Verwenden Sie Reflexion, die Auswirkungen auf die Leistung haben wird.

Code-Schnipsel:

import java.util.HashMap;

interface Visitable{
    void accept(Visitor visitor);
}

interface Visitor{
    void logGameStatistics(Chess chess);
    void logGameStatistics(Checkers checkers);
    void logGameStatistics(Ludo ludo);    
}
class GameVisitor implements Visitor{
    public void logGameStatistics(Chess chess){
        System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");    
    }
    public void logGameStatistics(Checkers checkers){
        System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");    
    }
    public void logGameStatistics(Ludo ludo){
        System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");    
    }
}

abstract class Game{
    // Add game related attributes and methods here
    public Game(){

    }
    public void getNextMove(){};
    public void makeNextMove(){}
    public abstract String getName();
}
class Chess extends Game implements Visitable{
    public String getName(){
        return Chess.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Checkers extends Game implements Visitable{
    public String getName(){
        return Checkers.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Ludo extends Game implements Visitable{
    public String getName(){
        return Ludo.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}

public class VisitorPattern{
    public static void main(String args[]){
        Visitor visitor = new GameVisitor();
        Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
        for (Visitable v : games){
            v.accept(visitor);
        }
    }
}

Erläuterung:

  1. Visitable ( Element ) ist eine Schnittstelle und diese Schnittstellenmethode muss zu einer Reihe von Klassen hinzugefügt werden.
  2. Visitor ist eine Schnittstelle, die Methoden zur Durchführung von Operationen auf Visitable Elemente.
  3. GameVisitor ist eine Klasse, die Folgendes implementiert Visitor Schnittstelle ( ConcreteVisitor ).
  4. Jede Visitable Element akzeptieren Visitor und rufen Sie eine entsprechende Methode von Visitor Schnittstelle.
  5. Sie können behandeln Game als Element und konkrete Spiele wie Chess,Checkers and Ludo als ConcreteElements .

Im obigen Beispiel, Chess, Checkers and Ludo sind drei verschiedene Spiele ( und Visitable Klassen). Eines schönen Tages bin ich auf ein Szenario gestoßen, bei dem ich die Statistiken der einzelnen Spiele protokollieren musste. Ohne einzelne Klassen zu modifizieren, um die Statistikfunktionalität zu implementieren, kann man diese Verantwortung in GameVisitor Klasse, die diese Aufgabe für Sie übernimmt, ohne die Struktur der einzelnen Spiele zu verändern.

Ausgabe:

Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser

Siehe

Oodesign-Artikel

Quellensuche article

für weitere Einzelheiten

Dekorateur

Muster ermöglicht es, einem einzelnen Objekt statisch oder dynamisch Verhalten hinzuzufügen, ohne das Verhalten anderer Objekte derselben Klasse zu beeinflussen

Verwandte Beiträge:

Decorator-Muster für IO

Wann sollte man das Decorator-Muster verwenden?

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X