Visitor Design Pattern in Java

1. Panoramica

In questo tutorial, introdurremo uno dei modelli di progettazione comportamentali di GoF: il visitatore.

Per prima cosa, spiegheremo il suo scopo e il problema che cerca di risolvere.

Successivamente, daremo uno sguardo al diagramma UML del visitatore e all'implementazione dell'esempio pratico.

2. Visitor Design Pattern

Lo scopo di un pattern Visitor è definire una nuova operazione senza introdurre le modifiche a una struttura di oggetti esistente.

Immagina di avere un compostooggetto costituito da componenti. La struttura dell'oggetto è fissa: non possiamo cambiarla o non abbiamo in programma di aggiungere nuovi tipi di elementi alla struttura.

Ora, come potremmo aggiungere nuove funzionalità al nostro codice senza modificare le classi esistenti?

Il modello di progettazione del visitatore potrebbe essere una risposta. In poche parole, non dovremo fare altro che aggiungere una funzione che accetti la classe del visitatore a ogni elemento della struttura.

In questo modo i nostri componenti consentiranno all'implementazione del visitatore di "visitarli" ed eseguire qualsiasi azione richiesta su quell'elemento.

In altre parole, estrarremo l'algoritmo che verrà applicato alla struttura dell'oggetto dalle classi.

Di conseguenza, faremo un buon uso del principio Aperto / Chiuso poiché non modificheremo il codice, ma saremo comunque in grado di estendere la funzionalità fornendo una nuova implementazione del visitatore .

3. Diagramma UML

Nel diagramma UML sopra, abbiamo due gerarchie di implementazione, visitatori specializzati ed elementi concreti.

Prima di tutto, il client utilizza un'implementazione Visitor e la applica alla struttura degli oggetti. L'oggetto composito itera sui suoi componenti e applica il visitatore a ciascuno di essi.

Ora, particolarmente rilevante è che gli elementi concreti (ConcreteElementA e ConcreteElementB) accettano un Visitatore, permettendogli semplicemente di visitarli .

Infine, questo metodo è uguale per tutti gli elementi della struttura, esegue un doppio invio con il passaggio (tramite questa parola chiave) al metodo di visita del visitatore.

4. Implementazione

Il nostro esempio sarà un oggetto Document personalizzato che consiste di elementi concreti JSON e XML; gli elementi hanno una superclasse astratta comune, l' Elemento.

La classe Document :

public class Document extends Element { List elements = new ArrayList(); // ... @Override public void accept(Visitor v) { for (Element e : this.elements) { e.accept(v); } } }

La classe Element ha un metodo astratto che accetta l' interfaccia Visitor :

public abstract void accept(Visitor v);

Pertanto, durante la creazione del nuovo elemento, denominalo JsonElement , dovremo fornire all'implementazione questo metodo.

Tuttavia, a causa della natura del pattern Visitor, l'implementazione sarà la stessa, quindi nella maggior parte dei casi ci richiederebbe di copiare e incollare il codice boilerplate da un altro elemento già esistente:

public class JsonElement extends Element { // ... public void accept(Visitor v) { v.visit(this); } }

Poiché i nostri elementi consentono di visitarli da qualsiasi visitatore, diciamo che vogliamo elaborare i nostri elementi del documento , ma ciascuno di essi in modo diverso, a seconda del tipo di classe.

Pertanto, il nostro visitatore avrà un metodo separato per il tipo specificato:

public class ElementVisitor implements Visitor { @Override public void visit(XmlElement xe) { System.out.println( "processing an XML element with uuid: " + xe.uuid); } @Override public void visit(JsonElement je) { System.out.println( "processing a JSON element with uuid: " + je.uuid); } }

Qui, il nostro visitatore concreto implementa due metodi, corrispondentemente uno per ogni tipo di Elemento .

Questo ci dà accesso al particolare oggetto della struttura su cui possiamo eseguire le azioni necessarie.

5. Test

A scopo di test, diamo un'occhiata alla classe VisitorDemo :

public class VisitorDemo { public static void main(String[] args) { Visitor v = new ElementVisitor(); Document d = new Document(generateUuid()); d.elements.add(new JsonElement(generateUuid())); d.elements.add(new JsonElement(generateUuid())); d.elements.add(new XmlElement(generateUuid())); d.accept(v); } // ... }

Per prima cosa, creiamo un Element Visitor, che contiene l'algoritmo che applicheremo ai nostri elementi.

Successivamente, impostiamo il nostro documento con i componenti appropriati e applichiamo il visitatore che sarà accettato da ogni elemento di una struttura di oggetti.

L'output sarebbe come questo:

processing a JSON element with uuid: fdbc75d0-5067-49df-9567-239f38f01b04 processing a JSON element with uuid: 81e6c856-ddaf-43d5-aec5-8ef977d3745e processing an XML element with uuid: 091bfcb8-2c68-491a-9308-4ada2687e203

Mostra che il visitatore ha visitato ogni elemento della nostra struttura, a seconda del tipo di elemento , ha inviato l'elaborazione al metodo appropriato e potrebbe recuperare i dati da ogni oggetto sottostante.

6. Aspetti negativi

Come ogni modello di progettazione, anche il visitatore ha i suoi svantaggi, in particolare, il suo utilizzo rende più difficile mantenere il codice se dobbiamo aggiungere nuovi elementi alla struttura dell'oggetto.

Ad esempio, se aggiungiamo un nuovo YamlElement, dobbiamo aggiornare tutti i visitatori esistenti con il nuovo metodo desiderato per l'elaborazione di questo elemento. Successivamente, se abbiamo dieci o più visitatori concreti, potrebbe essere complicato aggiornarli tutti.

Oltre a questo, quando si utilizza questo modello, la logica di business relativa a un particolare oggetto viene distribuita su tutte le implementazioni dei visitatori.

7. Conclusione

Il pattern Visitor è ottimo per separare l'algoritmo dalle classi su cui opera. Oltre a ciò, rende più facile l'aggiunta di nuove operazioni, semplicemente fornendo una nuova implementazione del Visitatore.

Inoltre, non dipendiamo dalle interfacce dei componenti e, se sono diverse, va bene, poiché abbiamo un algoritmo separato per l'elaborazione per elemento concreto.

Inoltre, il Visitatore può eventualmente aggregare i dati in base all'elemento che attraversa.

Per vedere una versione più specializzata del modello di progettazione del visitatore, controlla il modello del visitatore in Java NIO - l'uso del modello nel JDK.

Come al solito, il codice completo è disponibile nel progetto Github.