Quando si può anche copiare con classe
Andremo ora a parlare di un pattern creazionale che ci permette di "copiare con classe". Sì, anche se sembra strano, il compito fondamentale di questo pattern è copiare. Sto parlando del Prototype Pattern.
Chiariamo subito questo concetto.
Quando si parla di creazione di oggetti, il nostro immaginario comune da programmatori ci fa pensare ad operatori new
ed in particolar modo pensiamo subito al miglior modo di definire un costruttore adeguato alla creazione dell'istanza in modo da fornire al client un meccanismo di creazione.
Il Prototype Pattern ci permette di costruire oggetti attraverso la clonazione di una classe presa come esempio.
Dando una definizione:
Il Prototype Pattern crea oggetti sulla base di un oggetto esistente mediante la clonazione.
Quando uso il Prototype Pattern?
- quando la composizione, la creazione e la rappresentazione dell'oggetto devono necessariamente essere separate;
- quando viene specificato quale classe creare a runtime;
- quando c'è la necessità di nascondere la complessità della creazione dell'oggetto al client;
- quando creare un oggetto è un'operazione alquanto complessa e quindi copiarlo è più conveniente;
- quando gli oggetti necessari sono simili a quelli esistenti;
- si vuole evitare la creazione di tante factory parallele come succede in Abstract Factory;
Diagramma UML
Capiamo l'UML
- Prototype: è la classe astratta che definisce il comportamento delle classi che la useranno come esempio. Deve esporre un metodo per copiarla;
- ConcretePrototype: definisce le classi concrete che estendono Prorotype;
- Client: si occupa di richiamare il metodo di clonazione sui ConcretePrototype;
Esempio
Andremo ora a definire un piccolo esempio che si occupa di popolare e gestire un'hashmap di computer che possono essere desktop, laptop e server.
TipoComputer.java
public enum TipoComputer { DESKTOP, LAPTOP, SERVER }
Abbiamo definito un'enum che utilizzeremo per identificare i tre tipi di computer. E' stata definita per comodità e non è indispensabile.
ComputerPrototype.java
public abstract class ComputerPrototype implements Cloneable { public String modello; public ComputerPrototype(String modello) { this.modello = modello; } public String getModello() { return modello; } public void setModello(String modello) { this.modello = modello; } @Override public ComputerPrototype clone(){ try{ return (ComputerPrototype) super.clone(); }catch(CloneNotSupportedException e){ System.out.println("Something went wrong!"); return null; } } public abstract void printModel(); }
Questa è la classe che rappresenta il Prototype ed espone un metodo per poter clonare le classi che la estenderanno. Il design originale di questo pattern creazionale prevede il fatto che venga definito appunto un metodo per la clonazione degli oggetti. In Java questo meccanismo è fornito dal metodo clone()
della superclasse Object.
Talvolta viene sconsigliato l'utilizzo di questo metodo, ma cercando di astrarre nella maniera migliore possibile il concetto di Prototype Pattern è possibile rendere la classe astratta al più sottoclasse di Object e quindi sfruttare senza problemi il metodo di clonazione. Infatti i problemi sorgono quando le gerarchie di classi sono piuttosto lunghe.
Desktop.java
public class Desktop extends ComputerPrototype { public Desktop(String modello) { super(modello); } @Override public void printModel() { System.out.println(modello); } }
Laptop.java
public class Laptop extends ComputerPrototype { public Laptop(String modello) { super(modello); } @Override public void printModel() { System.out.println(modello); } }
Server.java
public class Server extends ComputerPrototype { public Server(String modello) { super(modello); } @Override public void printModel() { System.out.println(modello); } }
Queste tre classi rappresentano le entità concrete che verranno realmente copiate.
Client.java
import java.util.HashMap; import java.util.Map; public class Client { private Map<Enum,ComputerPrototype> computers = new HashMap<>(); public ComputerPrototype getComputer(TipoComputer tipo){ ComputerPrototype computer=computers.get(tipo); if(computer==null){ return null; } return computer.clone(); } public void populateMap(){ computers.put(TipoComputer.DESKTOP, new Desktop("MSI")); computers.put(TipoComputer.LAPTOP, new Laptop("Asus")); computers.put(TipoComputer.SERVER, new Server("Microsoft")); } }
In questa classe è definito un oggetto map che contiene oggetti di tipo ComputerPrototype identificati da un membro dell'enum dando origine così alla coppia <Enum, ComputerPrototype>.
Il metodo populateMap()
popola la mappa con tre oggetti, uno per ogni elemento dell'enum. Il metodo getComputer()
invece restituisce la copia di un oggetto che stiamo cercando in base al suo identificatore nell'hashmap se e solo se esiste, altrimenti restituisce null.
Main.java
public class Main { public static void main(String[] args){ Client client=new Client(); client.populateMap(); ComputerPrototype desktop=client.getComputer(TipoComputer.DESKTOP); System.out.println(desktop.getModello()); desktop.printType(); ComputerPrototype server = client.getComputer(TipoComputer.SERVER); System.out.println(server.getModello()); server.printType(); } }
Nel main ci limitiamo a creare un costruttore e a popolare l'hashmap. Dopodichè sfruttiamo i metodi definiti nel client per estrarre dati dalla mappa.
Osservazioni
Prima di tutto, precisiamo subito che l'esempio non è particolarmente significativo in quanto a funzionalità, ma si presta bene a mostrare la struttura del pattern.
Il metodo clone
è sovrascritto rispetto a quello della classe Object in modo da fornire una copia adatta alle nostre esigenze, copiando l'oggetto nella maniera che riteniamo più opportuna. Nell'esempio ho sovrascritto il metodo andando a controllare l'esistenza dell'oggetto estratto. In base al risultato del controllo viene restituita una copia dell'oggetto estratto se esiste, null altrimenti.
Una cosa importante da capire, e questo discorso vale per qualsiasi design pattern, è che nella stragrande maggioranza dei casi non sono legati al linguaggio. Infatti in questo particolare caso si sfrutta il metodo clone()
della classe Object, ma in un ipotetico linguaggio X che non dispone di questo metodo, il pattern è comunque applicabile usando un qualsiasi modo, purchè efficace, di copiare l'oggetto. L'unica limitazione evidente è che il linguaggio deve supportare l'OOP, in caso contrario non è il pattern specifico a non essere applicabile, ma è proprio la logica dei design patterns a non essere fruibile.
Conclusioni
Abbiamo visto come il Prototype Pattern permetta di creare istanze di oggetti a partire da istanze esistenti evitando soprattutto la costruzione di factory parallele.
Un'applicazione che ho trovato interessante, è quella in cui le classi vengono create a runtime. Utilizzando un meccanismo di RTTI insieme ad un corretto uso di Prototype siamo in grado di sviluppare applicazioni che creino ed utilizzino dinamicamente le istanze.
Il meccanismo RTTI è un meccanismo che permette di determinare il tipo di oggetto da creare durante l'esecuzione, senza la necessità di saperlo prima.
Attenzione, il meccanismo di RTTI deve essere usato con cautela, perchè si potrebbe sfruttare per caricare un ConcretePrototype con codice malevolo.
Per qualsiasi approfondimento, consiglio la lettura del testo sacro dei design patterns: Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm.