La Reflection è senz‘altro un modo utile per ottenere, a runtime, informazioni dettagliate su una classe ed è possibile usarla per creare oggetti dinamicamente, anche quando la classe di tale oggetti non è nota a tempo di compilazione. Tuttavia, se lo scopo che ci si prefigge è creare dinamicamente degli oggetti che hanno un comportamento comune, può essere vantaggiosamente impiegato il pattern Abstract Factory.
Esempio: un lettore di documenti universale
Supponiamo di voler implementare un sistema per l‘apertura di formati di documenti di diverso tipo: RTF, DOC e PDF [3]. Definiamo intanto un‘interfaccia comune che espone il metodo di apertura di un documento, il cui nome è passato come parametro, che tutti i documenti concreti devono implementare:
public interface Document { public void openDocument(String documentName); }
La classe di un generico documento, che implementa l‘interfaccia, sarà così fatta:
public class PDFDocument { public void openDocument(String documentName) { // ... elaborazione del file ... System.out.println("Apro il documento PDF: "+documentName); } }
Avremo, quindi, tante classi quanti sono i formati di documento che vogliamo far supportare al nostro lettore. Ogni classe implementa il metodo dell‘interfaccia per garantire l‘apertura del particolare formato di documento. Per esempio, la classe di sopra potrebbe usare la libreria iText per gestire i documenti PDF, mentre la classe DOCDocument potrebbe richiamare le API di Jakarta POI per gestire i file Word. Per semplicità , i metodi stamperanno solo una stringa con il nome del documento. Ai fini del nostro esempio, le classi DOCDocument ed RTFDocument saranno simili.
Primo approccio: la Reflection
Vediamo allora come realizzare la creazione dinamica di oggetti di tipo Document con la Reflection. Definiamo la classe DocumentManager che crea un‘istanza concreta di una delle tre classi viste nel paragrafo precedente ed apre un particolare documento. Per far ciò, definiamo un metodo che riceve, come parametri, il nome fisico della classe da istanziare ed il nome del documento:
public class DocumentManager { public void open(String documentType, String documentName) throws Exception { Class docClass = Class.forName("it.ep.reflection."+documentType); Document realDocument = (Document)docClass.cast(docClass.newInstance()); realDocument.openDocument(documentName); } }
Si noti che è indispensabile fornire il percorso completo della classe che si deve istanziare quando si richiama il metodo forName(…) della classe Class. Con la seconda istruzione si crea un documento concreto, forzando il cast al tipo Document. Infine sull‘oggetto appena creato, possiamo invocare il metodo openDocument(…). Una ipotetica sequenza di esecuzione potrebbe essere la seguente:
/* creazione dinamica di documenti */ DocumentManager dm = new DocumentManager(); dm.open("PDFDocument", "filepdf.pdf"); dm.open("DOCDocument", "filedoc.doc"); dm.open("RTFDocument", "rtf.rtf");
Osserviamo subito la semplicità di questo approccio, in quanto sarà la Java Virtual Machine a preoccuparsi di istanziare correttamente, a run time, una delle tre classi concrete, in base al parametro documentType che viene passato al metodo open(…). D‘altra parte, con questo approccio, si introduce un certo tempo di over head dovuto proprio al recupero della classe fisica ed alla creazione di una sua nuova istanza. Inoltre, l‘utilizzo della Reflection è strettamente vincolato a Java! E ancora, cosa succederebbe se Java non fornisse nativamente questo strumento? Questi problemi possono essere risolti ricorrendo al pattern Abstract Factory.
Secondo approccio: Abstract Factory Pattern
Questo pattern creazionale appartiene alla ben nota lista dei 23 pattern di GoF [1]. Lo scopo del pattern è di presentare un‘interfaccia per la creazione di famiglie di prodotti (oggetti), in modo che il client che li utilizza non conosca affatto le loro classi concrete. Questo porta indubbiamente due vantaggi:
- il client può utilizzare solo prodotti tra loro vincolati;
- il client può utilizzare diverse famiglie di prodotti.
Se pensiamo al nostro esempio, siamo proprio nel campo di applicabilità di questo pattern. Infatti, abbiamo famiglie di documenti diversi. Ogni famiglia è composta da un tipo di documento e, soprattutto, offre la stessa interfaccia: un client può richiedere l‘apertura di un qualunque documento appartenente ad una di queste famiglie. Il problema consiste proprio nella creazione di famiglie di prodotti (documenti), senza legare il codice del client a quello della particolare famiglia.
Il pattern Abstract Factory fornisce una soluzione semplice ed elegante: si creano delle interfacce per ogni tipo di prodotto! Si realizzano poi dei prodotti concreti che implementano queste interfacce. Le famiglie di prodotti vengono create da un oggetto particolare: il factory. Ogni famiglia avrà un factory per creare le istanze dei prodotti. Per non legare il client al particolare factory, quest‘ultimo implementa una interfaccia comune, nota al client.
La struttura del pattern è quella di figura 1.
L‘applicazione del pattern al nostro esempio è riportata in figura 2.
Vediamo nel dettaglio i partecipanti al pattern:
- AbstractFactory (interfaccia DocumentFactory): definisce un metodo per creare e restituire nuovi documenti. Il tipo restituito è AbstractProduct.
- ConcreteFactory (classi PDFDocumentFactory, RTFDocumentFactory e DOCDocumentFactory): implementano l‘AbstractFactory, fornendo il metodo per creare e restituire documenti concreti – ConcreteProduct.
- AbstractProduct (interfaccia Document): definisce le operazioni comuni ai diversi documenti. Nel nostro caso, il metodo per l‘apertura del documento.
- ConcreteProduct (classi PDFDocument, RTFDocument e DOCDocument): definiscono i documenti concreti, creati da ogni ConcreteFactory.
- Client (classe DocumentManager): utilizza l‘AbstractFactory per rivolgersi alle ConcreteFactory di un particolare documento. Può utilizzare i diversi documenti attraverso la loro interfaccia comune fornita da AbstractProduct, invocando quindi il metodo di apertura del documento. Questo è il punto più geniale ed elegante del pattern: infatti, il client crea nuove istanze di documenti senza realmente sapere quale sta utilizzando in un particolare istante, grazie all‘utilizzo del DocumentFactory.
Notiamo subito che nel primo paragrafo abbiamo definito un‘interfaccia e tre classi. Bene, l‘interfaccia Document è l‘AbstractProduct, mentre PDFDocument, RTFDocument e DOCDocument sono i ConcreteProduct. In definitiva “manca” solo il codice che rende dinamica la creazione dei documenti, che è esattamente la parte che viene gestita automaticamente dalla Reflection, come abbiamo visto nel paragrafo precedente. Creiamo quindi le classi e le interfacce restanti del pattern per avere il tassello mancante, cioè definiamo i factory ed il client.
Definiamo intanto l‘interfaccia DocumentFactory:
public interface DocumentFactory { public Document createDocument(); }
In essa viene definito il metodo che crea e restituisce documenti. Questa interfaccia verrà poi utilizzata dal DocumentManager per creare dinamicamente nuovi documenti, su cui invocare l‘operazione di lettura. Adesso codifichiamo i ConcreteFactory che permettono di creare le nuove istanze di documenti:
public class PDFDocumentFactory implements DocumentFactory { public Document createDocument() { return new PDFDocument(); } }
Ogni factory, uno per ogni tipo di ConcreteProduct, implementa l‘interfaccia dell‘AbstractFactory: non fa altro che restituire una nuova istanza del ConcreteProduct ad essa associato. Ovviamente il codice di RTFDocumentFactory e DOCDocumentFactory è analogo.
Vediamo il codice del client:
public class DocumentManager { public void selectDocument(DocumentFactory documentType) { this.documentType = documentType; } public void open(String documentName) { Document document = documentType.createDocument(); document.openDocument(documentName); } private DocumentFactory documentType; }
Il primo metodo è usato per ricevere un oggetto di una particolare famiglia di documenti, cioè un ConcreteFactory, e lo registra nel proprio attributo documentType. Il secondo metodo serve invece ad istanziare un particolare documento, cioè un ConcreteProduct, su cui invocare il metodo di apertura. Si noti ancora, come il client utilizzi i documenti senza sapere quali siano, cioè senza sapere quale istanza particolare del ConcreteFactory è contenuta in documentType.
Vediamo infine una possibile sequenza di utilizzi del client:
DocumentManager dm = new DocumentManager(); dm.selectDocument(new PDFDocumentFactory()); dm.open("filepdf.pdf"); dm.selectDocument(new DOCDocumentFactory()); dm.open("filedoc.doc"); dm.selectDocument(new RTFDocumentFactory()); dm.open("filertf.rtf");
E questo è il risulato che volevamo raggiungere: siamo in grado adesso di creare dinamicamente documenti di tipo diverso, utilizzando il DocumentManager, il quale, effettivamente, non ha conoscenze sulla classe concreta che sta utilizzando. Siamo così riusciti a raggiungere il nostro scopo, senza far ricorso alla Reflection!
Conclusioni
Abbiamo visto come il pattern Abstract Factory possa essere impiegato efficacemente quando dobbiamo gestire delle famiglie di oggetti che hanno un comportamento comune. Con questo approccio evitiamo l‘utilizzo della Reflection, rendendo così il codice svincolato da un meccanismo proprio di Java e portabile verso altri linguaggi Object Oriented. Non per ultimo, si evita l‘over head insito nel meccanismo di Reflection [4].
Riferimenti
[1] E. Gamma, R. Helm, R.Johnson, J.Vlissides, “Design Pattern. Elements of Reusable Object-Oriented Software”, Addison-Wesley, 1995.
[2] Lorenzo Bettini, “Una fabbrica di oggetti”, MokaByte 27, febbraio 1999
https://www.mokabyte.it/1999/02/index.htm
[3] Sandro Pedrazzini, “Realizzazione di un Framework. Gerarchie separate e costruttori virtuali”, MokaByte 58, dicembre 2001
https://www.mokabyte.it/2001/12/index.htm
[4] Robert Berger, “Tuning Java Code”, Berger Tecnologies Inc., Novembre 2004
http://www.pghtech.org/networks/informationsystems/files/pittjug-nov04.pdf