Quando si pensa ad un oggetto, si associa ad esso il concetto di creazione con l‘operatore new. Prototype fornisce un approccio alternativo alla creazione, sfruttando la clonazione. Si vedrà come sia possibile costruire, utilizzando questo pattern, un sistema di caricamento dinamico di plugin.
Quasi sempre, quando si implementa una classe, si definisce in essa un costruttore per fornire, al client che la utilizza, un metodo per inizializzare il suo stato. Un approccio alternativo, fornito dal pattern Prototype, consiste nella creazione delle istanze attraverso la clonazione di una classe presa come esempio. Vedremo come sfruttare tale pattern e il meccanismo di RTTI (Run Time Type Identification) di Java per creare un lettore di documenti modificabile a run time.
Appicabilità di Prototype
Questo pattern creazionale, proposto da GoF [1], si applica nelle situazioni in cui si vuole presentare al client un‘interfaccia per la creazione di un insieme di oggetti, nascondendone le classi concrete. Notiamo subito che l‘intento del pattern è identico a quello di Abstract Factory (AF) [2] e per questo motivo i due pattern entrano spesso in conflitto. Tuttavia, l‘applicazione di Prototype si rende necessaria, e quindi alternativa ad AF, nei casi in cui:
- le classi sono specificate a run time;
- si vuole evitare la costruzione di factory parallele: si ricordi che in AF, diversamente, si creano tante factory quanti sono i ConcreteProduct che si intende realizzare;
- le istanze delle classi hanno un insieme ridotto di stati.
Il diagramma UML del pattern è quello di figura 1.
Figura 1 – La struttura del pattern Prototype
Vediamo nel dettaglio chi sono i partecipanti:
- Prototype (classe astratta): definisce il comportamento comune a tutte le classi che la useranno come esempio. Questa classe deve definire un metodo per fornire copie di sà© stessa.
- ConcretePrototype (classe concreta): definisce la classe concreta (o le classi) che estenderanno il Prototype.
- Client (classe concreta): richiama il metodo di clonazione sui ConcretePrototype.
Il design originale di questo pattern [1] richiede che il Prototype definisca una operazione di clonazione per ottenere le nuove istanze delle classi. Tale operazione, in Java, è fornita dal metodo clone() della superclasse Object. Tuttavia, in alcuni testi come [3], viene sconsigliato il ricorso al metodo citato prima per evitare di ottenere oggetti con troppi attributi, e si consiglia piuttosto la creazione ex novo dell‘oggetto con l‘operatore new. Questo discorso è corretto ovviamente se la gerarchia di ereditarietà è molto profonda e, soprattutto, se si implementa il Prototype come classe concreta. Ma se riusciamo ad astrarre il concetto di Prototype, e quindi a renderlo una classe astratta al più sottoclasse di Object, possiamo sfruttare senza alcun problema la clonazione fornita nativamente dal linguaggio.
In [1], viene consigliato di utilizzare una classe che funga da Prototype Manager per tener traccia dei ConcretePrototype, il cui numero potrebbe non essere fissato a tempo di compilazione. Utilizzeremo, nell‘esempio che segue, proprio questo approccio.
Il lettore universale di documenti rivisitato
Riprendiamo l‘esempio visto in [2]. Vogliamo aggiungere al nostro lettore universale di documenti due funzionalità :
- la possibilità di definire a run time il tipo di documento da leggere;
- la definizione a run time di un nuovo lettore, cioè di un nuovo ConcretePrototype, senza la necessità di definire una nuova factory.
Vediamo come si possono risolvere questi problemi applicando il pattern Prototype.
Definiamo intanto il diagramma UML utilizzando il pattern:
Figura 2 – Applicazione del pattern all‘esempio
Vediamo nel dettaglio le singole classi:
- DocumentPrototype (classe astratta): rappresenta il generico documento, costituito dalle proprietà contenuto e tipo, visibili da ogni documento concreto. Pensando un attimo al concetto di documento, ci accorgiamo che ha senso definirlo come classe astratta piuttosto che come classe concreta, perché non avremmo alcuna idea su quali potrebbero essere il suo tipo e, soprattutto, il suo contenuto, nel momento in cui dovessimo crearlo. Forniamo quindi il metodo astratto di apertura di un documento, che ogni classe concreta saprà come implementare. Inoltre, mettiamo a disposizione del client i due getter per gli attributi. Infine, definiamo il metodo che consente di ottenere delle copie di questo prototipo: attraverso il metodo concreto cloneDocument() otteniamo i cloni del documento, ricorrendo alla clonazione fornita da Java.
- PDFDocument (classe concreta): realizza il ConcretePrototype, implementando il metodo astratto della classe base openDocument(…) e fornendo, così, il meccanismo di apertura dei documenti PDF.
- RTFDocument (classe concreta): realizza il ConcretePrototype. Simile alla precedente, ma gestisce i file RTF.
- DOCDocument (classe concreta): è il ConcretePrototype che fornisce il supporto ai documenti in formato MS Word.
- PrototypeManager (classe concreta): si occupa della gestione diretta dei ConcretePrototype, in modo da evitare che sia il Client ad avere la responsabilità di creare i prototipi. Si rende quindi necessario definire una struttura dati che contenga le istanze dei prototipi: la scelta è ricaduta su una HashMap, che usa come chiave primaria il tipo di documento che implementa. Con il metodo privato initPrototypes(), il costruttore inizializza l‘array associativo. Si noti che questo meccanismo di creazione della mappa può essere reso più sicuro con l‘utilizzo del pattern Singleton. Tuttavia, per il nostro scopo, ne faremo a meno. La classe fornisce, inoltre, il metodo getDocument(…) che si preoccupa di cercare nella mappa il tipo di documento passato come parametro e di restituirne una copia. Infine, espone il metodo addDocumentPrototype(…) che consente di aggiungere a runtime una nuova entry nella mappa, cioè una nuova coppia . Per rendere possibile tale funzionalità al client, è stato utilizzato il meccanismo di RTTI, cioè l‘identificazione dinamica dei tipi, fornita dalla meta classe Class [4].
- Client (classe concreta): invoca i metodi della classe precedente per poter leggere i documenti ed, eventualmente, per aggiungere nuovi lettori a runtime.
Implementazione del lettore.
Vediamo adesso l‘implementazione della soluzione descritta nel paragrafo precedente.
Iniziamo dalla classe DocumentPrototype:
package mb_prototype; public abstract class DocumentPrototype implements Cloneable { protected String type; protected String documentContent; public abstract void openDocument(String documentName); public String getType() { return type; } public String getDocumentContent() { return documentContent; } public DocumentPrototype cloneDocument() throws CloneNotSupportedException { DocumentPrototype clone = (DocumentPrototype)super.clone(); return clone; } }
Il generico documento di esempio è rappresentato da questa classe astratta che implementa l‘interfaccia Cloneable per rendere possibile l‘invocazione, nel metodo cloneDocument(), di clone() della classe base Object. Si noti che il metodo openDocument(…) è astratto proprio perché, a questo livello, non sapremmo come implementarlo!
Vediamo quindi come sono fatti i documenti concreti, cioè i ConcretePrototype, che estendono la classe vista sopra.
Cominciamo con PDFDocument:
package mb_prototype; import java.io.IOException; import java.util.List; import org.pdfbox.pdmodel.PDDocument; import org.pdfbox.util.PDFTextStripper; public class PDFDocument extends DocumentPrototype{ public PDFDocument() { type = Constants.PDF_FILE; } public void openDocument(String documentName) { String realName = documentName+"."+getType(); PDDocument document = null; PDFTextStripper stripper = null; try { document = PDDocument.load(realName); stripper = new PDFTextStripper(); documentContent = stripper.getText(document); } catch (IOException ex) { ex.printStackTrace(); } finally { try { document.close(); } catch (IOException ex) { ex.printStackTrace(); } } } }
In essa è definito un costruttore di default che inizializza il tipo di documento (attributo type) a PDF. Attraverso la libreria PDFBox [5], viene eseguita l‘apertura e la lettura del documento, il cui contenuto è memorizzato nell‘attributo documentContent, ereditato dalla classe base DocumentPrototype.
DOCDocument e RTFDocument sono simili; ciò che le differisce dalla classe precedente è ovviamente il metodo di apertura del documento:
package mb_prototype; import java.io.FileInputStream; import java.io.IOException; import org.apache.poi.hwpf.HWPFDocument; import org.apache.poi.hwpf.extractor.WordExtractor; public class DOCDocument extends DocumentPrototype { public DOCDocument() { type = Constants.DOC_FILE; } public void openDocument(String documentName) { String realName = documentName+"."+getType(); HWPFDocument document = null; WordExtractor docContent = null; FileInputStream fis = null; try { fis = new FileInputStream(realName); document = new HWPFDocument(fis); docContent = new WordExtractor(document); documentContent = docContent.getText(); } catch (Exception ex) { ex.printStackTrace(); } finally { try { fis.close(); } catch (IOException ex) { ex.printStackTrace(); } } } }
Per l‘apertura dei file DOC si è fatto ricorso alla libreria Jakarta POI [6].
L‘apertura dei file RTF, invece, non richiede alcuna libreria esterna:
package mb_prototype; import java.io.FileInputStream; import java.io.IOException; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.rtf.RTFEditorKit; public class RTFDocument extends DocumentPrototype { public RTFDocument() { type = Constants.RTF_FILE; } public void openDocument(String documentName) { String realName = documentName+"."+getType(); RTFEditorKit rtfReader = new RTFEditorKit(); DefaultStyledDocument document = new DefaultStyledDocument(); FileInputStream fis = null; try { fis = new FileInputStream(realName); rtfReader.read(fis, document, 0); documentContent = document.getText(0, document.getLength()); } catch (Exception ex) { ex.printStackTrace(); } finally { try { fis.close(); } catch (IOException ex) { ex.printStackTrace(); } } } }
Trattiamo adesso il PrototypeManager:
package mb_prototype; import java.util.HashMap; import java.util.Map; public class PrototypeManager { private Map documentPrototypes; // Migliorabile con Singleton public PrototypeManager() { initPrototypes(); } private void initPrototypes() { documentPrototypes = new HashMap(); documentPrototypes.put("pdf", new PDFDocument()); documentPrototypes.put("rtf", new RTFDocument()); } public void addDocumentPrototype(String documentType, String documentReader) { try { // RTTI - No Reflection Class c = Class.forName(documentReader); DocumentPrototype dp = (DocumentPrototype)c.newInstance(); documentPrototypes.put(documentType, dp); }catch (Exception ex) { ex.printStackTrace(); } } public DocumentPrototype getDocument(String documentType) throws CloneNotSupportedException, DocumentNotSupportedException { DocumentPrototype dp = null; try { dp = ((DocumentPrototype)documentPrototypes.get(documentType)).cloneDocument(); } catch (NullPointerException npe) { throw new DocumentNotSupportedException(); } return dp; } }
Nella mappa documentPrototypes si mantengono i riferimenti ai ConcretePrototype, indicizzati dalla loro estensione. Attraverso il metodo initPrototypes() si inizializza la mappa per supportare i file PDF e RTF. Il metodo addDocumentPrototype(…) permette di aggiungere a runtime una nuova entry nella mappa documentPrototype, per supportare così un nuovo formato di documento. Si noti che, attraverso il meccanismo di RTTI, si carica a runtime la classe passata come parametro e se ne crea una nuova istanza. Ovviamente, affinchà© questo meccanismo funzioni, è evidente che la classe che si aggiunge alla mappa deve estendere la classe DocumentPrototype e implementare quindi il metodo openDocument(…).
Il metodo getDocument(…) esegue il lookup del documentType, passato come parametro, nella tabella documentPrototypes e richiama il metodo cloneDocument() di DocumentPrototype per fornire una nuova copia del documento. Nel momento in cui non venisse trovato il documentType nella mappa, verrebbe sollevata l‘eccezione DocumentNotSupportedException, così definita:
package mb_prototype;public class DocumentNotSupportedException extends Exception { public DocumentNotSupportedException() { super("Formato di documento non supportato!"); } } Vediamo infine il Client: package mb_prototype; import java.io.Console; public class Client { public static void main(String[] args) throws CloneNotSupportedException, DocumentNotSupportedException { String str; String documentRealName; String documentContent = new String(); String documentType = new String(); DocumentPrototype document; PrototypeManager pm = new PrototypeManager(); String[] documentNameSplit = null; do { Console cons; cons = System.console(); str = cons.readLine(); // F | nome documento.estensione if (str.matches("F")) { // legge il nome del file . String nomeFile = cons.readLine(); documentNameSplit = nomeFile.split("\."); document = pm.getDocument(documentNameSplit[1]); document.openDocument(documentNameSplit[0]); documentContent = document.getDocumentContent(); documentType = document.getType(); System.out.println(" Il file aperto è un "+documentType+ ". Il contenuto è questo: "+documentContent); } // N | estensione documento | classe reale if (str.matches("N")) { // String type = cons.readLine(); // String realClass = cons.readLine(); pm.addDocumentPrototype(type, realClass); } } while (!str.equals("E")); } }
Il client crea una nuova istanza di PrototypeManager per interagire con i ConcreteFactory. Come si può facilmente osservare, in esso non compare alcun riferimento ai ConcreteFactory. La sua logica è abbastanza semplice: attraverso l‘oggetto Console (di Java 6) vengono letti dei caratteri da tastiera. Se il carattere è “F”, viene letto il nome del file: da esso si estraggono il nome del file e l‘estensione. Proprio attraverso l‘estensione, il PropotypeManager recupera il ConcreteFactory corretto, eseguendo la lookup sull‘array associativo documentPrototypes, per l‘apertura del particolare formato di file. Sul riferimento al documento si applica quindi il metodo di apertura del file, openDocument(…), al quale viene passato il nome del file fisico. Dal documento document si recuperano e si stampano a video il tipo di file ed il contenuto del file appena aperto.
Digitando invece il carattere “N”, possiamo caricare nel PrototypeManager un nuovo lettore per un particolare formato: inseriamo dapprima il tipo di documento (p.e. doc) e poi il nome reale del ConcreteFactory (p.e. mb_prototype.DOCDocument). A questo punto il PrototypeManager aggiunge una nuova entry nella mappa dei prototipi.
Digitando “E” si termina l‘esecuzione.
Vediamo alcuni esempi:
Se tentiamo di aprire un file di tipo DOC, entriamo ovviamente in eccezione perché il ConcreteFactory per il tipo “doc” non è stato associato nel metodo initPrototypes() del PrototypeManager a compile time:
Figura 4 – Apertura di un formato di documento non supportato
Associando invece il tipo “doc” alla classe DOCDocument a run time, siamo in grado di leggere anche i file Word:
Figura 5 – Apertura di un documento Word
Conclusioni
Prototype fornisce una modo efficace di creare istanze di classi a partire da una classe presa come esempio e, soprattutto, evita la costruzione di factory parallele, come avviene in AF. L‘applicazione più interessante del pattern, come abbiamo avuto modo di vedere, consiste nella creazione di istanze di una classe che sono note solo a run time. In questo modo, otteniamo un codice che istanzia classi dinamicamente, senza ricorrere a costrutti condizionali. Con il meccanismo di RTTI, unitamente all‘uso di un PrototypeManager, siamo in grado di costruire ed utilizzare dinamicamente istanze di un ConcretePrototype, ottenendo così un sistema di caricamento dinamico di plugin. Si noti però che il meccanismo di RTTI deve essere usato con cautela, in quanto un client potrebbe caricare dinamicamente nel PrototypeManager un ConcretePrototype contenente codice dannoso!
Riferimenti
[1] E. Gamma – R. Helm – R. Johnson – J. Vlissides, “Design Pattern: Elements of Reusable Object-Oriented Software”, Addison-Wesley, 1995
[2] E. Polito, “Reflection e pattern Abstract Factory”, Mokabyte 123, Novembre 2007
https://www.mokabyte.it/cms/section.run?name=mb123
[3] S. Metsker, “Design Pattern in Java”, Pearson Addison-Wesley, 2003
[4] B. Eckel, “Thinking in Java”
www.mindview.net/Books/TIJ/
[5] PDFBox – Java PDF Library
http://www.pdfbox.org
[6] Apache POI – Java API To Access Microsoft Format Files
http://poi.apache.org