Java Design Pattern: Prototype Pattern

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

Prototype Pattern UML

Capiamo l'UML

  1. Prototype: è la classe astratta che definisce il comportamento delle classi che la useranno come esempio. Deve esporre un metodo per copiarla;
  2. ConcretePrototype: definisce le classi concrete che estendono Prorotype;
  3. 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.

 
 
Alessio Mungelli

Alessio Mungelli

Computer Science student at UniTo (University of Turin), Network specializtion, blogger and writer. I am a kind of expert in Java desktop developement with interests in AI and web developement. Unix lover (but not Windows hater). I am interested in Linux scripting. I am very inquisitive and I love learning new stuffs.

 
 
 

Articoli correlati

    Un approccio a Java: strutture di selezione - casi d'uso

    Ciao a tutti e bentornati! Sino ad ora ci siamo preoccupati di fare una carrellata quanto più completa riguardo i concetti fondamentali di cui abbiamo bisogno per approcciarci all'utilizzo delle…

    Un approccio a Java: operatori booleani

    La volta precedente, abbiamo ampiamente parlato delle variabili booleane, cercando di delineare quali siano le principali operazioni che si possono effettuare proprio a livello pratico.  Di tutti i casi esaminati, non abbiamo…

    Un approccio a Java: le variabili booleane

    Ciao a tutti e bentornati! La volta precedente, ho fatto un'introduzione alle strutture condizionali, definendo il loro funzionamento. Prima di poter dare qualche esempio pratico, è necessario chiarire in che modo ci…

    Un approccio a Java: strutture condizionali

    Ciao a tutti e bentornati! Le volte precedenti abbiamo introdotto il concetto di variabile, tentando di definire alcuni concetti basilari a riguardo.  Alcune situazioni fanno però intuire come il solo concetto…

    Un approccio a Java: Le variabili - caso d'uso

    Ciao a tutti amici e bentornati! Dopo l'introduzione fatta sulle variabili, cerchiamo di analizzare alcune criticità che si possono presentare in situazioni alquanto comuni. Partiamo quindi analizzando degli esempi pratici.  Esempio 1: divisione…

    Un approccio a Java: Le variabili

    Ciao a tutti e bentornati! Quest'oggi inizieremo un percorso che ci porterà a studiare, ed eventualmente ripassare, quelle che sono le basi della programmazione. Inizieremo parlando di variaibli. Introduzione Chiunque voglia approcciarsi al…

    Hashmap con Concatenamento: hashing, collisioni e prime funzioni

    Oggi andremo a vedere dei concetti strettamente legati alle hashmap. I concetti che andremo a vedere sono quelli di hashing e collisioni. Hashing L'idea dell'hashing con concatenamento è quella di creare una sorta di array di…

    Hashmap con concatenamento: liste di trabocco

    In questa breve serie di articoli andremo a vedere com'è possibile realizzare in C la struttura dati Hashmap. Nell'implementazione andremo ad usare le liste doppiamente concatenate come strutture dati ausiliarie. Andiamo…

    Linux per Principianti: I permessi

    Nei precedenti articoli abbiamo fatto una breve introduzione al mondo Unix e nell'articolo successivo abbiamo parlato di comandi base per la gestione del file system. Oggi andremo a parlare di permessi. Come esempio prenderemo sempre…

    Linux Per Principianti: Terminale Ubuntu

    Ho introdotto nell'articolo precedente, consultabile qui, i concetti base relativi al mondo del pinguino. Oggi andiamo a vedere alcune operazioni di base che si possono svolgere mediante linea di comando su…

    Linux per Principianti: Introduzione

    Se hai pensato di migrare da Windows a un sistema operativo Unix, o Linux nello specifico ci sono cose che dovresti sapere. L'obiettivo è quello di dare le informazioni essenziali…

    Java Strutture Dati: Liste Concatenate

    Con il 2020 andiamo ad esaminare un nuovo aspetto della programmazione: le strutture dati. Spesso capita a tutti di utilizzare strutture messe a disposizione dai vari linguaggi di programmazione. L'obiettivo sarà…