Home > GOF Pattern, java > GoF Patterns: Prototype

GoF Patterns: Prototype

18 gennaio 2011

Translate in English with Google Translate
In questo articolo tratterò il pattern Prototype

Motivazione

Si tratta di un pattern creazionale basato su oggetti e viene utilizzato per creare un nuovo oggetto clonando un oggetto già esistente detto prototipo. Questo pattern risulta utile affinchè il Client possa creare nuovi oggetti senza conoscerne i dettagli implementativi ma avvalendosi della clonazione. La creazione del clone avviene a RunTime e non a CompileTime pertanto il clone viene creato in sede di esecuzione.
Durante la creazione del clone dell’oggetto occorre prestare molta attenzione alla creazione degli oggetti annidati. Una classe può contenere al suo interno dei riferimenti ad altre classi pertanto la clonazione dell’oggetto principale deve effettuare la clonazione anche di tutti gli altri oggetti al suo interno. La clonazione dell’intero albero degli oggetti genera un clone detto deep-clone ossia clone-approfondito in quanto copia di tutti gli oggetti presenti. Se la clonazione si limita solo all’oggetto principale “contenitore” allora nel clone verranno mantenuti gli stessi  riferimenti agli oggetti secondari in questo caso si parla di shallow-clone ossia di clone-superficiale. In seguito vediamo un esempio di deep-copy in java.

Partecipanti e Struttura

Questo pattern è composto dai seguenti partecipanti:

  1. Prototype: dichiara una interfaccia per la clonazione
  2. ConcretePrototype: implementa un’operazione di clonazione di se stesso
  3. Client: invoca la clonazione dell’oggetto
Prototype Pattern

Prototype Pattern

Conseguenze

Tale pattern presenta i seguenti vantaggi/svantaggi:

  1. aggiungere/rimuovere prodotti a RunTime: è possibile decidere a Runtime se aggiungere nuovi oggetti
  2. specificare nuovi oggetti cambiando il loro valore: invece di creare nuove classi per definire nuovi comportamenti, possiamo cambiare il valore di un oggetto per definire un nuovo comportamento. In questo modo vengono ridotti i numeri delle classi.

Implementazione

Come esempio pensiamo al caso in cui vogliamo creare dei template di tabelle di Hash da poter essere facilmente clonate all’occorrenza. In Java una tabella di hash può essere realizzata utilizzando per esempio le classi HashMap, IdentityHashMap, LinkedHashMap ognuna con caratteristiche diverse. Realizziamo una classe astratta Hash e specializziamo le classi di hash di nostro interesse.

Vediamo come si presenta in UML in base all’esempio:

Esempio Prototype Pattern

Esempio Prototype Pattern

La classe astratta Hash implementa l’interfaccia Clonable per dichiarare la propria volontà di clonazione. Questa interfaccia è molto simile all’interfaccia Serializable, anch’essa è vuota:

package java.lang;
public interface Cloneable {
}

La classe astratta Hash ereditando di default dalla classe Object, eredita il metodo protected native Object clone() throws CloneNotSupportedException; che implementa di default la clonazione. Nel nostro caso richiameremo semplicemente il metodo nativo super.clone()

package patterns.prototype;

public abstract class Hash implements Cloneable {

    @Override
    public Object clone() throws CloneNotSupportedException{
        return super.clone();
    }

    public abstract void addItem(Object key, Object value);

    public abstract int getSize();

}

Le classi seguenti MyLinkedHashMap, MyHashMap e MyIdentityHashMap ereditano dalla classe Hash ed effettuano l’overriding del metodo clone() SENZA deep-copy MA shadow-copy ossia non duplicano gli oggetti contenuti in esse.

package patterns.prototype;

import java.util.LinkedHashMap;

public class MyLinkedHashMap extends Hash {

    private LinkedHashMap hash = new LinkedHashMap();

    @Override
    public Object clone() throws CloneNotSupportedException{
        return (MyLinkedHashMap) super.clone();
    }

    @Override
    public void addItem(Object key, Object value) {
        hash.put(key, value);
    }

    @Override
    public int getSize() {
        return hash.size();
    }

}
package patterns.prototype;

import java.util.HashMap;

public class MyHashMap extends Hash {

    private HashMap hash = new HashMap();

    @Override
    public Object clone() throws CloneNotSupportedException {
        return (MyHashMap) super.clone();
    }

    @Override
    public void addItem(Object key, Object value) {
        hash.put(key, value);
    }

    @Override
    public int getSize() {
        return hash.size();
    }
}
package patterns.prototype;

import java.util.IdentityHashMap;

public class MyIdentityHashMap extends Hash {

    private IdentityHashMap hash = new IdentityHashMap();

    @Override
    public Object clone() throws CloneNotSupportedException {
        return (MyIdentityHashMap) super.clone();
    }

    @Override
    public void addItem(Object key, Object value) {
        hash.put(key, value);
    }

    @Override
    public int getSize() {
        return hash.size();
    }
}

La classe Client ha il compito di invocare il template di interesse e richiedere la clonazione.
Nel nostro esempio viene creato un template della classe MyLinkedHashMap per poi essere popolato con una chiave ed a questo punto viene clonato l’oggetto. Abbiamo fatto una shallow-copy ossia una clonazione superficiale.

  • L’oggetto MyLinkedHashMap è stato duplicato e ce ne accorgiamo dal fatto che l’hashcode è diverso.
  • L’oggetto annidato LinkedHashMap non è stato clonato e ce ne accorgiamo dal fatto che se proviamo ad aggiungere una nuova chiave al template questa viene “ritrovata” anche nell’oggetto clonato.
package patterns.prototype;

public class Client {

    public static void main(String[] args) throws CloneNotSupportedException {

        Hash hash = new MyLinkedHashMap();
        hash.addItem("key1", "value1");

        System.out.println( "Prototype");
        System.out.println( "ClassName: " + hash.getClass().getCanonicalName() );
        System.out.println( "ClassHashCode:" + hash.hashCode() );

        Hash hashCloned = (MyLinkedHashMap) hash.clone();
        System.out.println( "Clone:");
        System.out.println( "ClassName: " + hashCloned.getClass().getCanonicalName() );
        System.out.println( "ClassHashCode:" + hashCloned.hashCode() );

        System.out.println( "Prototype Hashtable size: " + hash.getSize() );
        System.out.println( "Cloned Hashtable size: " + hashCloned.getSize() );

        System.out.println( "Adding new key");
        hash.addItem("key2", "value2");

        System.out.println( "Prototype Hashtable size: " + hash.getSize() );
        System.out.println( "Cloned Hashtable size: " + hashCloned.getSize() );

    }

}
$JAVA_HOME/bin/java patterns.prototype.Client
Prototype
ClassName: patterns.prototype.A
ClassHashCode:16130931
Clone:
ClassName: patterns.prototype.A
ClassHashCode:26315233
Prototype Hashtable size: 1
Cloned Hashtable size: 1
Adding new key
Prototype Hashtable size: 2
Cloned Hashtable size: 2

Guardando l’Object Diagram abbiamo questa situazione di condivisione dell’oggetto hash:

Object Shared

Object Shared

Per evitare questo problema occorre prevedere la deep-copy definiendo nell’overriding del metodo clone() la clonazione anche dell’oggetto annidato. Pertanto in tutte le classi che ereditano direttamente dalla classe astratta Hash modifichiamo il metodo clone() in questo modo:

    @Override
    public Object clone() throws CloneNotSupportedException{
        MyLinkedHashMap myLinkedHashMap = (MyLinkedHashMap) super.clone();
        myLinkedHashMap.hash = (LinkedHashMap) myLinkedHashMap.hash.clone();
        return myLinkedHashMap;
    }

In questo modo quando eseguiamo la classe Client vediamo che l’inserimento di una nuova chiave nella nostra tabella di hash di template non determina alcuna modifica nella tabella di hash clonata

$JAVA_HOME/bin/java patterns.prototype.Client
Prototype
ClassName: patterns.prototype.MyLinkedHashMap
ClassHashCode:16130931
Clone:
ClassName: patterns.prototype.MyLinkedHashMap
ClassHashCode:23660326
Prototype Hashtable size: 1
Cloned Hashtable size: 1
Adding new key
Prototype Hashtable size: 2
Cloned Hashtable size: 1

Guardando l’Object Diagram abbiamo questa situazione di NON condivisione dell’oggetto hash:

Object Not Shared

Object Not Shared

A seconda della complessità dell’oggetto template ed a seconda dell’alberatura annidata degli oggetti presenti nella classe template, occorrerà ricordarsi di clonare tutti gli oggetti presenti nel template.

Categorie:GOF Pattern, java
  1. Francesco
    18 ottobre 2014 alle 12:44 PM

    Ovviamente “myLinkedHashMap.hash” nella deep clone non lo puoi fare perchè l’oggetto contenuto ‘hash’ l’hai dichiarato privato😉 Nemmeno ‘public’ andrebbe bene perchè violerebbe l’incapsulamento …. e quindi mi sa che devi rivedere meglio la soluzione🙂 Ciao.

    • 19 ottobre 2014 alle 9:25 AM

      Uhm, perchè no? myLinkedHashMap.hash è presente nel metodo clone() della classe MyLinkedHashMap e punta ad una proprietà privata ma visibile a livello di classe. La versione integrale sarebbe questa:

      package patterns.prototype;
       
      import java.util.LinkedHashMap;
       
      public class MyLinkedHashMap extends Hash {
       
          private LinkedHashMap hash = new LinkedHashMap();
       
          @Override
          public Object clone() throws CloneNotSupportedException{
              MyLinkedHashMap myLinkedHashMap = (MyLinkedHashMap) super.clone();
              myLinkedHashMap.hash = (LinkedHashMap) myLinkedHashMap.hash.clone();
          return myLinkedHashMap;
          }
          
          @Override
          public void addItem(Object key, Object value) {
              hash.put(key, value);
          }
       
          @Override
          public int getSize() {
              return hash.size();
          }
       
      }
      

      L’obiettivo del metodo MyLinkedHashMap.clone() è di creare una nuova istanza di se stessa e di LinkedHashMap ossia del suo ramo presente.
      Difatti la stampa a video mostra che l’inserimento di una nuova chiave nella lista hash(MyLinkedHashMap.hash) presente nella pima istanza (hash) non determina l’inserimento nella lista (MyLinkedHashMap.hash) della seconda istanza creata (hashCloned) in quanto si tratta di oggetti diversi, ossia clonati.
      Fammi sapere se va bene o mi è sfuggito qualcosa.
      Grazie del tuo commento. Ciao.

      • Francesco
        19 ottobre 2014 alle 10:24 AM

        Anzitutto ti ringrazio per la replica.

        E’ la visibilità di ‘hash’ ad essere a livello di classe; com’è possibile che myLinkedHashMap.hash, variabile di un diverso oggetto, abbia la visibilità di cui parli?
        Magari è a me che sta sfuggendo qualcosa (e nel merito ti chiederei la cortesia di darmi dei riferimenti) ….

        In pratica tu nella ‘deep clone()’ non accedi alla variabile ‘hash’ della classe in cui tale metodo è definito; tu prima crei un nuovo oggetto … e poi vuoi settare una *sua* variabile membro privata in maniera diretta.

        Perciò dico, magari è a me che sfugge qualcosa per cui mi risulta strano quell’accesso (non ho ancora testato il codice).

        Ciò che si fa è creare e settare proprio un nuovo oggetto, e poi si setta l’oggetto come membro del suo contenitore da clonare. Tutto questo senza effettuare quel tipo di accesso.
        Esempi (a parte l’esistente libreria open source Java Deep-Cloning):
        http://howtodoinjava.com/2012/11/08/a-guide-to-object-cloning-in-java/ ;
        soluzione con gli -Stream :- http://www.javaworld.com/article/2077578/learn-java/java-tip-76–an-alternative-to-the-deep-copy-technique.html ;
        (un altro metodo molto semplice di cui non trovo più il link…).

        Ti ringrazio per l’attenzione.

        Ciao.

      • 19 ottobre 2014 alle 12:07 PM

        Credo di aver capito meglio il tuo dubbio, spero🙂
        Clonare una classe significa copiare il contenuto di byte in memoria e passarli ad una nuova reference:
        http://stackoverflow.com/questions/5375311/how-does-object-class-implement-clone-method
        Una volta che possiedi la reference, l’accesso alle proprietà/metodi dipendono dalla loro visibilità, ok.
        Poichè stiamo creando una istanza di una classe DENTRO la classe stessa, ossia di se stessa, allora abbiamo la completa visibilità anche delle proprietà private.
        Per spiegarmi meglio ho creato una classe di esempio, paragonando l’invocazione del costruttore con il cloning di un oggetto.
        In entrambi i casi le 2 istanze della classe IstanzaInterna, poichè sono state create DENTRO la classe IstanzaInterna quindi al loro interno, hanno visibilità delle proprietà private.

        public class IstanzaInterna implements Cloneable {
        
            private String x = "Esempio" ;
            
            public static void main(String[] args) throws CloneNotSupportedException {
                new IstanzaInterna().test();
            }
            
            private void test() throws CloneNotSupportedException{
                IstanzaInterna istanza1 = new IstanzaInterna();
                System.out.println( istanza1.x );
                
                IstanzaInterna istanza2 = (IstanzaInterna)super.clone();
                System.out.println( istanza2.x );        
            }
        
        }
        

        Output:

        Esempio
        Esempio
        

        Mentre invece se creo una seconda classe IstanzaEsterna, poichè crea una istanza di una classe diversa da se stessa e poi cerca di referenziare una sua proprietà privata, allora ottiene l’errore in fase di compilazione.

        public class IstanzaEsterna {
        
            private String x = "Esempio" ;
            
            public static void main(String[] args) throws CloneNotSupportedException {
                new IstanzaEsterna().test();
            }
            
            private void test() throws CloneNotSupportedException{
                IstanzaInterna istanza1 = new IstanzaInterna();
                System.out.println( istanza1.x );
                
                IstanzaInterna istanza2 = (IstanzaInterna)super.clone();
                System.out.println( istanza2.x );        
            }
        }
        

        Output:

        Exception in thread "main" java.lang.RuntimeException: Uncompilable source code - x has private access in IstanzaInterna
        	at IstanzaEsterna.test(IstanzaEsterna.java:11)
        	at IstanzaEsterna.main(IstanzaEsterna.java:6)
        Java Result: 1
        

        Quindi il tuo dubbio era lecito, funzionava perchè la visibilità private è consentita dall’interno della classe.
        Spero di aver capito il tuo dubbio ed essere riuscito a spiegarmi meglio, altrimenti fammi sapere.
        Ciao

      • Francesco
        19 ottobre 2014 alle 3:56 PM

        Ora è chiaro. Ho fatto anche una prova e funziona. Hai compreso in pieno il mio dubbio.

        Tuttavia, pur essendo lecito scrivere ‘istanza1.x’ (con ‘x’ privata), forse anche per evitare di generare dubbi simili al mio, la deep clone() viene generalmente implementata in uno dei modi che indicavo ( -Stream, librerie, o più semplicemente creando e settando a mano nuovi oggetti).

        Ti ringrazio per l’attenzione e la gentilezza mostratami.

        Ciao.

      • 20 ottobre 2014 alle 11:30 PM

        Perfetto, sono contento di aver capito il tuo dubbio.🙂
        Ho guardato gli articoli relativi al deep-clone degli oggetti, interessanti, anche se il risultato non è sicuro e la tecnica proposta non è convincente.
        L’approccio che usano è di utilizzare Reflection + ( Bytecode Injection ?) + Serializzazione + Clonable = Troppo consumo di risorse.
        Oltretutto gli oggetti non serializzabili per natura resterebbero fuori, i Singleton/enum/ConstantPool dovrebbero essere gestiti ad-hoc, non mi convince!
        Secondo me, se le classi da clonare sono tante allora io consiglierei un altro approccio: clonare il classloader! In questo modo verrebbero copiati i bytes di tutte le classi in un colpo solo, one-shot, senza fare deep-inspection, senza esaminare e modificare la struttura delle classi (aggiungendo implements Serializable/Cloneable, costruttori, metodi ecc) inoltre con poco consumo di risorse ed includendo anche gli oggetti non serializzabili e non disturbando i Singleton, oltretutto senza fare uso della serializzazione se non per copiare il classloader, approccio OSGI.
        Guarda questo progetto, fa questo: https://code.google.com/p/transloader
        Ciao

  1. No trackbacks yet.
I commenti sono chiusi.
%d blogger cliccano Mi Piace per questo: