Java-Tipp 98: Denken Sie über das Besucherentwurfsmuster nach

Sammlungen werden häufig in der objektorientierten Programmierung verwendet und werfen häufig Fragen zum Code auf. Beispiel: "Wie führen Sie eine Operation für eine Sammlung verschiedener Objekte aus?"

Ein Ansatz besteht darin, jedes Element in der Sammlung zu durchlaufen und dann basierend auf seiner Klasse für jedes Element etwas Spezifisches zu tun. Das kann ziemlich schwierig werden, besonders wenn Sie nicht wissen, welche Art von Objekten sich in der Sammlung befinden. Wenn Sie die Elemente in der Sammlung ausdrucken möchten, können Sie eine Methode wie die folgende schreiben:

public void messyPrintCollection (Sammlungssammlung) {Iterator iterator = collection.iterator () while (iterator.hasNext ()) System.out.println (iterator.next (). toString ())} 

Das scheint einfach genug. Sie rufen einfach die Object.toString()Methode auf und drucken das Objekt aus, oder? Was ist, wenn Sie beispielsweise einen Vektor von Hashtabellen haben? Dann werden die Dinge immer komplizierter. Sie müssen den Objekttyp überprüfen, der von der Sammlung zurückgegeben wird:

public void messyPrintCollection (Sammlungssammlung) {Iterator iterator = collection.iterator () while (iterator.hasNext ()) {Object o = iterator.next (); if (o Instanz der Sammlung) messyPrintCollection ((Sammlung) o); sonst System.out.println (o.toString ()); }}

OK, jetzt haben Sie verschachtelte Sammlungen verarbeitet, aber was ist mit anderen Objekten, die nicht das zurückgeben String, was Sie von ihnen benötigen? Was ist, wenn Sie Anführungszeichen um StringObjekte hinzufügen und ein f nach FloatObjekten hinzufügen möchten ? Der Code wird noch komplexer:

public void messyPrintCollection (Sammlungssammlung) {Iterator iterator = collection.iterator () while (iterator.hasNext ()) {Object o = iterator.next (); if (o Instanz der Sammlung) messyPrintCollection ((Sammlung) o); sonst wenn (o Instanz von String) System.out.println ("'" + o.toString () + "'"); sonst wenn (o Instanz von Float) System.out.println (o.toString () + "f"); sonst System.out.println (o.toString ()); }}

Sie können sehen, dass die Dinge sehr schnell kompliziert werden können. Sie wollen keinen Code mit einer riesigen Liste von if-else-Anweisungen! Wie vermeidest du das? Das Besuchermuster kommt zur Rettung.

Um das Besuchermuster zu implementieren, erstellen Sie eine VisitorSchnittstelle für den Besucher und eine VisitableSchnittstelle für die zu besuchende Sammlung. Sie haben dann konkrete Klassen, die die Schnittstellen Visitorund Visitableimplementieren. Die beiden Schnittstellen sehen folgendermaßen aus:

öffentliche Schnittstelle Besucher {public void visitCollection (Sammlungssammlung); public void visitString (String string); public void visitFloat (Float float); } public interface Visitable {public void accept (Besucher Besucher); }}

Für einen Beton Stringkönnten Sie haben:

öffentliche Klasse VisitableString implementiert Visitable {private String value; public VisitableString (String string) {value = string; } public void accept (Besucher Besucher) {visit.visitString (this); }}

In der Akzeptanzmethode rufen Sie die richtige Besuchermethode für den thisTyp auf:

besucher.visitString (this) 

Auf diese Weise können Sie einen konkreten Vorgang Visitorwie folgt implementieren :

öffentliche Klasse PrintVisitor implementiert Visitor {public void visitCollection (Sammlungssammlung) {Iterator iterator = collection.iterator () while (iterator.hasNext ()) {Object o = iterator.next (); if (o Instanz von Visitable) ((Visitable) o) .accept (this); } public void visitString (String string) {System.out.println ("'" + string + "'"); } public void visitFloat (Float float) {System.out.println (float.toString () + "f"); }}

Wenn Sie dann eine VisitableFloatKlasse und eine VisitableCollectionKlasse implementieren , die jeweils die entsprechenden Besuchermethoden aufrufen, erhalten Sie das gleiche Ergebnis wie bei der chaotischen if-else- messyPrintCollectionMethode, jedoch mit einem viel saubereren Ansatz. In visitCollection()rufen Sie auf Visitable.accept(this), was wiederum die richtige Besuchermethode aufruft. Das nennt man Doppelversand; Das Visitorruft eine Methode in der VisitableKlasse auf, die in die VisitorKlasse zurückruft.

Obwohl Sie eine if-else-Anweisung durch die Implementierung des Besuchers bereinigt haben, haben Sie dennoch viel zusätzlichen Code eingeführt. Sie mussten Ihre ursprünglichen Objekte Stringund FloatObjekte, die die VisitableSchnittstelle implementieren, umschließen. Obwohl dies ärgerlich ist, ist dies normalerweise kein Problem, da die Sammlungen, die Sie normalerweise besuchen, nur Objekte enthalten können, die die VisitableSchnittstelle implementieren .

Trotzdem scheint es viel zusätzliche Arbeit zu sein. Schlimmer noch, was passiert, wenn Sie beispielsweise einen neuen VisitableTyp hinzufügen VisitableInteger? Dies ist ein Hauptnachteil des Besuchermusters. Wenn Sie ein neues VisitableObjekt hinzufügen möchten , müssen Sie die VisitorSchnittstelle ändern und diese Methode dann in jeder Ihrer VisitorImplementierungsklassen implementieren. Sie Visitorkönnen anstelle einer Schnittstelle eine abstrakte Basisklasse mit standardmäßigen No-Op-Funktionen verwenden. Das wäre ähnlich wie bei den AdapterKlassen in Java-GUIs. Das Problem bei diesem Ansatz besteht darin, dass Sie Ihre einzelne Vererbung verbrauchen müssen, die Sie häufig für etwas anderes speichern möchten, z. B. für die Erweiterung StringWriter. Es würde Sie auch darauf beschränken, nur VisitableObjekte erfolgreich besuchen zu können.

Glücklicherweise können Sie mit Java das Besuchermuster viel flexibler gestalten, sodass Sie VisitableObjekte nach Belieben hinzufügen können. Wie? Die Antwort ist durch Reflexion. Mit a ReflectiveVisitorbenötigen Sie nur eine Methode in Ihrer Benutzeroberfläche:

öffentliche Schnittstelle ReflectiveVisitor {öffentlicher nichtiger Besuch (Objekt o); }}

OK, das war einfach genug. Visitablekann gleich bleiben, und ich werde gleich darauf zurückkommen. Im Moment werde ich PrintVisitormithilfe von Reflection implementieren :

public class PrintVisitor implementiert ReflectiveVisitor {public void visitCollection (Sammlungssammlung) {... wie oben ...} public void visitString (String string) {... wie oben ...} public void visitFloat (Float float) { ... wie oben ...} public void default (Objekt o) {System.out.println (o.toString ()); } public void visit (Objekt o) {// Class.getName () gibt auch Paketinformationen zurück. // Dadurch werden die Paketinformationen entfernt, die uns // nur den Klassennamen geben. String methodName = o.getClass (). GetName (); methodName = "visit" + methodName.substring (methodName.lastIndexOf ('.') + 1); // Jetzt versuchen wir, die Methode visit aufzurufen. Try {// Ruft die Methode visitFoo (Foo foo) ab. Methode m = getClass (). GetMethod (methodName, new Class [] {o.getClass ()}); // Versuche visitFoo (Foo foo) aufzurufen m.invoke (this, new Object [] {o});} catch (NoSuchMethodException e) {// Keine Methode, ebenso die Standardimplementierung default (o); }}}

Jetzt brauchen Sie die VisitableWrapper-Klasse nicht mehr. Sie können einfach anrufen visit(), und es wird an die richtige Methode gesendet. Ein schöner Aspekt ist, dass der visit()Versand nach Belieben erfolgen kann. Es muss keine Reflexion verwendet werden - es kann einen völlig anderen Mechanismus verwenden.

Mit dem neuen PrintVisitorhaben Sie Methoden für Collections, Stringsund Floats, aber dann fangen Sie alle nicht behandelten Typen in der catch-Anweisung ab. Sie werden die visit()Methode erweitern, damit Sie auch alle Superklassen ausprobieren können. Zuerst fügen Sie eine neue Methode mit dem Namen hinzu getMethod(Class c), die die aufzurufende Methode zurückgibt und nach einer passenden Methode für alle Oberklassen von Class cund dann für alle Schnittstellen für sucht Class c.

geschützte Methode getMethod (Klasse c) {Klasse newc = c; Methode m = null; // Probiere die Superklassen aus, während (m == null && newc! = Object.class) {String method = newc.getName (); method = "visit" + method.substring (method.lastIndexOf ('.') + 1); try {m = getClass (). getMethod (Methode, neue Klasse [] {newc}); } catch (NoSuchMethodException e) {newc = newc.getSuperclass (); }} // Probiere die Schnittstellen aus. Falls erforderlich, // können Sie sie zuerst sortieren, um "besuchbare" Schnittstellengewinne zu definieren, // falls ein Objekt mehrere implementiert. if (newc == Object.class) {Class [] interfaces = c.getInterfaces (); for (int i = 0; i <interfaces.length; i ++) {String method = interfaces [i] .getName (); method = "visit" + method.substring (method.lastIndexOf ('.') + 1); try {m = getClass (). getMethod (Methode, neue Klasse [] {interfaces [i]});} catch (NoSuchMethodException e) {}}} if (m == null) {try {m = thisclass.getMethod ("visitObject", neue Klasse [] {Object.class}); } catch (Ausnahme e) {// Kann nicht passieren}} return m; }}

Es sieht kompliziert aus, ist es aber nicht. Grundsätzlich suchen Sie nur nach Methoden, die auf dem Namen der Klasse basieren, die Sie übergeben haben. Wenn Sie keine finden, probieren Sie deren Oberklassen aus. Wenn Sie keine finden, probieren Sie alle Schnittstellen aus. Zuletzt können Sie es einfach visitObject()als Standard versuchen .

Beachten Sie, dass ich für diejenigen, die mit dem traditionellen Besuchermuster vertraut sind, dieselbe Namenskonvention für die Methodennamen befolgt habe. Wie einige von Ihnen vielleicht bemerkt haben, wäre es jedoch effizienter, alle Methoden "visit" zu nennen und den Parametertyp als Unterscheidungsmerkmal zu verwenden. Wenn Sie dies jedoch tun, stellen Sie sicher, dass Sie den visit(Object o)Namen der Hauptmethode in so etwas wie ändern dispatch(Object o). Andernfalls haben Sie keine Standardmethode, auf die Sie zurückgreifen können, und Sie müssen bei Objectjedem Aufruf visit(Object o)eine Umwandlung vornehmen, um sicherzustellen, dass das richtige Methodenaufrufmuster befolgt wurde.

Jetzt ändern Sie die visit()Methode, um Folgendes zu nutzen getMethod():

public void visit (Objektobjekt) {try {Method method = getMethod (getClass (), object.getClass ()); method.invoke (dies, neues Objekt [] {Objekt}); } catch (Ausnahme e) {}}

Jetzt ist Ihr Besucherobjekt viel leistungsfähiger. Sie können jedes beliebige Objekt übergeben und eine Methode verwenden, die es verwendet. Darüber hinaus profitieren Sie von einer Standardmethode visitObject(Object o), mit der Sie alles erfassen können, was Sie nicht angeben. Mit etwas mehr Arbeit können Sie sogar eine Methode für hinzufügen visitNull().

Ich habe die VisitableSchnittstelle aus einem bestimmten Grund dort belassen. Ein weiterer Nebeneffekt des traditionellen Besuchermusters besteht darin, dass die VisitableObjekte die Navigation in der Objektstruktur steuern können. Wenn Sie beispielsweise ein TreeNodeObjekt implementiert haben Visitable, können Sie eine accept()Methode verwenden, die den linken und rechten Knoten durchläuft:

public void accept (Besucher Besucher) {visit.visitTreeNode (this); besucher.visitTreeNode (leftsubtree); besucher.visitTreeNode (rightsubtree); }}

Mit nur einer weiteren Änderung an der VisitorKlasse können Sie eine Visitablekontrollierte Navigation ermöglichen:

public void visit (Objektobjekt) löst eine Ausnahme aus {Method method = getMethod (getClass (), object.getClass ()); method.invoke (dies, neues Objekt [] {Objekt}); if (Objektinstanz von Visitable) {callAccept ((Visitable) Objekt); }} public void callAccept (Visitable visitable) {visitable.accept (this); }}

Wenn Sie eine VisitableObjektstruktur implementiert haben, können Sie die callAccept()Methode Visitableunverändert lassen und die kontrollierte Navigation verwenden. Wenn Sie innerhalb des Besuchers durch die Struktur navigieren möchten, überschreiben Sie einfach die callAccept()Methode, um nichts zu tun.

Die Kraft des Besuchermusters kommt zum Tragen, wenn mehrere verschiedene Besucher in derselben Objektsammlung verwendet werden. Ich habe beispielsweise einen Interpreter, einen Infix-Writer, einen Postfix-Writer, einen XML-Writer und einen SQL-Writer, die für dieselbe Sammlung von Objekten arbeiten. Ich könnte leicht einen Präfixschreiber oder einen SOAP-Schreiber für dieselbe Sammlung von Objekten schreiben. Außerdem können diese Autoren elegant mit Objekten arbeiten, von denen sie nichts wissen, oder, wenn ich es wähle, eine Ausnahme auslösen.

Fazit

Durch die Verwendung von Java Reflection können Sie das Besucherentwurfsmuster verbessern, um eine leistungsstarke Möglichkeit zum Bearbeiten von Objektstrukturen bereitzustellen und die Flexibilität zu bieten, neue hinzuzufügen

Visitable

Typen nach Bedarf. Ich hoffe, Sie können dieses Muster irgendwo auf Ihren Codierungsreisen verwenden.

Jeremy Blosser programmiert seit fünf Jahren in Java und hat während dieser Zeit für verschiedene Softwareunternehmen gearbeitet. Heute arbeitet er für ein Startup-Unternehmen, Software Instruments. Sie können Jeremys Website unter //www.blosser.org besuchen.

Erfahren Sie mehr über dieses Thema

  • Muster-Homepage

    //www.hillside.net/patterns/

  • EntwurfsmusterElemente wiederverwendbarer objektorientierter Software, Erich Gamma, et al. (Addison-Wesley, 1995)

    //www.amazon.com/exec/obidos/ASIN/0201633612/o/qid=963253562/sr=2-1/002-9334573-2800059

  • Muster in Java, Band 1, Mark Grand (John Wiley & Sons, 1998)

    //www.amazon.com/exec/obidos/ASIN/0471258393/o/qid=962224460/sr=2-1/104-2583450-5558345

  • Muster in Java, Band 2, Mark Grand (John Wiley & Sons, 1999)

    //www.amazon.com/exec/obidos/ASIN/0471258415/qid=962224460/sr=1-4/104-2583450-5558345

  • Zeigen Sie alle vorherigen Java-Tipps an und senden Sie Ihre eigenen

    //www.javaworld.com/javatips/jw-javatips.index.html

Diese Geschichte "Java-Tipp 98: Reflect on the Visitor Design Pattern" wurde ursprünglich von JavaWorld veröffentlicht.