Articoli Manifesto Tools Links Canali Libri Contatti ?
Design / Design Patterns

Oggetti Immutabili

Abstract
L'idea di Incapsulamento dello stato di un Oggetto è uno dei cardini della Object Technology. Vedendo un Oggetto in isolamento il concetto sembra potersi risolvere facilmente ma, cosa accade quando un Oggetto è condiviso con altri? Una risposta sono gli Oggetti Immutabili.
Data di stesura: 01/04/2005
Data di pubblicazione: 10/06/2005
Ultima modifica: 04/04/2006
di Stefano Fago Discuti sul forum   Stampa

Introduzione

La discussione che segue affronta due temi: le argomentazioni legate al preservare l'incapsulamento dello stato di un oggetto e quelle relative all'uso e applicabilità di Oggetti Immutabili. Verranno espressi alcuni accorgimenti di design e di programmazione atti a sviluppare entrambe le tematiche.

Problemi dell'Accesso Controllato

Lo sviluppatore ad Oggetti è consapevole dell'importanza dei metodi di accesso per proteggere lo stato "privato" di un elemento: il software di oggi vede un'abbondante presenza di forme di questo tipo:

  1. public class MyObject 
  2.   private String name = ... 
  3.   private HashMap properties = ... 
  4.  
  5.   ... 
  6.  
  7.   public String getName() 
  8.     return n; 
  9.  
  10.   public HashMap getProperties() 
  11.     return p; 
  12.  
  13.   public void setName(String n) 
  14.     this.n  = n; 
  15.  
  16.   public void setProperties(HashMap p) 
  17.     this.p = p; 
  18.  
  19.   ... 

Questa idea viene rafforzata da alcune specifiche, nei differenti linguaggi, che fanno della naming convention sui Getter/Setter la base per la capacità di esplorare componenti di cui non si conosce il sorgente, come nel caso dei JavaBean!

La realizzazione dell'incapsulamento in questo modo è immediata ma presenta dei limiti con un oggetto non più banale:

  1. la presenza di una coppia di metodi per ogni proprietà porta al proliferare di codice rendendo l'oggetto FAT!
  2. Se le proprietà di un oggetto non sono solo primitivi, i semplici metodi di accesso possono risultare inutili: riprendendo il codice appena letto, l'attributo PROPERTIES può essere tranquillamente corrotto dall'esterno nonostante i Get/Set...Si presenta come se fosse stato dichiarato pubblico!

Chi vieta, infatti, un'azione del tipo:

  1. myObjectInstance.getProperties().put("IMPORTANT_ATTR", WRONG_VALUE); 

senza quindi utilizzare un opportuno Setter?!?!

Proteggiamo lo stato di un Oggetto: Defensive Copy e Oggetti Immutabili

Una semplice soluzione alla problematica appena emersa consiste nell'azione di copia o cloning degli elementi di stato. Quello che viene proposto è una tecnica secondo la quale nei metodi di GET viene ad essere restituito non il vero attributo ma una sua copia:

  1. public HashMap getProperties() 
  2.   return p.clone(); // oppure return new HashMap(p); 

Quanto esposto rimane valido anche se, invece di considerare un singolo attributo, prendiamo in esame l'intero stato di un oggetto, a sua volta espresso come istanza di una specifica classe:

  1. public class MyObjectStatus 
  2.   ... 
  3.  
  4. public class MyObject 
  5.   private  MyObjectStatus  instanceStatus = ... 
  6.  
  7.   ... 
  8.  
  9.   public MyObjectStatus getObjectStatus() 
  10.   {  
  11.     return  instanceStatus.clone(); 
  12.  
  13.   ... 

La tecnica di defensive copy anche in questo caso ci protegge da un un uso improprio dello stato di un oggetto!

Questa idea sembra del tutto funzionale, ma è facile evidenziarne alcuni difetti:

  1. si determina la creazione di molti oggetti temporanei e, su un numero sensibile di attributi, il processo potrebbe divenire dispendioso in termini di memoria
  2. l'attività di cloning può risultare problematica se l'oggetto da clonare è complesso: questo generalmente è evidente quando lo stato di un oggetto è espresso da strutture dati ricorsive o annidate ad N livelli.
  3. legato alla correttezza del Design, vi è un altro aspetto. La forma utilizzata per l'attributo PROPERTIES pone in evidenza che il nostro Oggetto usa una data tipologia di mappa come proprio "internal": lì dove è possibile, è preferibile evitare che un metodo suggerisca il COME di un'astrazione, discorso questo che risulta importante se si decide di variare il tipo di ritorno del Getter e se la nostra astrazione è la root-class di una gerarchia(vedesi il problema: fragilità della classe base)!

Quanto detto può portarci a formulare il precedente metodo nella forma:

  1. public Map getProperties() 
  2.   return p.clone(); // oppure return new HashMap(p); 

Un altro approccio per preservare lo stato del nostro Oggetto è quello di renderlo immutabile: un'astrazione in sola lettura dovrebbe permettere di avere degli elementi consistenti nel tempo ed essere, per sua natura, condivisibile e thread-safe!

Questa idea è molto diffusa ed è punto di discussione in molte realizzazione: basti pensare alla controversa natura degli oggetti String in Java.

Un Oggetto Immutabile è formulato generalmente con la sola presenza di metodi di GET ed un costruttore che permette il setup dei valori delle variabili di stato altrimenti non attribuibili; a volte i costruttori sono più di uno: in alcuni casi si preferisce fornire un COPY CONSTRUCTOR che permette di ottenere un Oggetto Immutabile da un altro per copia.

Supponiamo di avere un Oggetto Immutabile che presenta la forma:

  1. public class MyObject 
  2.   private final String n; 
  3.   private final Map p; 
  4.  
  5.   public MyObject(String n, Map p) 
  6.   {  
  7.     this.n = n; 
  8.     this.p = p;  
  9.  
  10.   public MyObject(MyObject mo) 
  11.     this.n = mo.getName(); 
  12.     this.p = mo.getProperties(); 
  13.  
  14.   public String getName() 
  15.     return n; 
  16.  
  17.   public Map getProperties() 
  18.     return p.clone(); 

Esaminiamo ora un possible uso pericoloso da parte di un altro sviluppatore:

  1. Map m = new HashMap(); 
  2.  
  3. MyObject obj = new MyObject("DEMO", m); 
  4.  
  5. ... 
  6.  
  7. m.put("IMPORTANT_ATTR", WRONG_VALUE); 

Notiamo che, anche se l'oggetto è immutabile, lo stato può essere ancora una volta corrotto!

È possibile, anche in questo caso, lavorare in "modo difensivo" è rafforzare il costruttore attuando una copy dei parametri complessi in ingresso:

  1. MyObject(String n, Map p) 
  2. {  
  3.   this.n = new String(n); 
  4.   this.p = new HashMap(p);  

Il nuovo costruttore fa sì che il codice scritto dal developer di terze parti, così come esposto prima, non abbia influenza sullo stato del nostro oggetto.

L'opportuna costruzione di un Oggetto Immutabile può essere resa più comoda e validante grazia all'adozione di una factory: un metodo dedicato permetterà di nascondere le problematiche creazionali permettendo pre-processamenti e controlli prima dell'effettiva definizione del nuovo oggetto. Riprendendo il nostro esempio possiamo ipotizzare allora del codice nella forma:

  1. public class MyObject 
  2.   private final String n; 
  3.   private final Map p; 
  4.  
  5.   private MyObject(String n, Map p) 
  6.   {  
  7.     this.n = n; 
  8.     this.p = p;  
  9.  
  10.   public static MyObject newObject(String arg1, Map arg2) 
  11.     String stringToPass = new String(arg1); 
  12.     Map   mapToPass2 = new HashMap(arg2); 
  13.  
  14.     return new MyObject(stringToPass, mapToPass); 
  15.  
  16.   ... 

Questa idea può essere estesa fino ad arrivare ad una factory-class(Mutable Companion Pattern) che permetta di simulare la presenza di Setter pur continuando a lavorare con oggetti immutabili:

  1. public class MyObjectCompanion 
  2.   private final String stringArg; 
  3.   private final Map mapArg; 
  4.  
  5.   public synchronized void setArgs(String data, Map map) 
  6.   {  
  7.     stringArg = data; 
  8.     mapArg = map;   
  9.  
  10.   public synchronized MyObject toObject() 
  11.     return new MyObject(stringArg, mapArg); 
  12.  
  13.   ... 

Riportiamo ora delle semplici linee guida che permettono di definire Oggetti Immutabili:

  • dichiarare la classe FINAL
  • dichiarare tutti gli attributi PRIVATE
  • dichiarare solo QUERY METHOD, cioè solo GETTER
  • dichiarare dei costruttori per l'inizializzazione dei dati

Queste linee guida possono essere completate da una serie di accorgimenti che determinano un enforcing della particolare natura dell'oggetto:

  • dichiarare tutti gli attributi anche FINAL
  • non usare il riferimento a THIS del costruttore
  • non restituire oggetti mutabili: usare la tecnica del defensive copy o restituire solo oggetti a loro volta immutabili

Usare gli Oggetti Immutabili

L'uso di oggetti immutabili ha sicuramente dei grossi limiti ed è, quindi, necessario valutare quando adottarli. Per loro natura, questi elementi sono ottimi quando l'obiettivo è la condivisione di valori e la loro dimensione è limitata: lo sharing di risorse nasce per migliorare il consumo di memoria allorché si evidenzi l'adozione di Oggetti Valore, come Date, Unità di Misura e simili, nel proprio sistema. È possibile distinguere due tipologie di resource-sharing: extrinsic-sharing e intrinsic-sharing.

Extrinsic Sharing

Adottando questa soluzione, l'utente non può creare direttamente un oggetto immutabile ma dovrà ricorrere ad un factory-method che prenderà in ingresso un dato token. All'interno della classe verrano mantenuti dei riferimenti agli oggetti creati in modo da riutilizzare le risorse già richieste: un nuovo oggetto verrà ad essere istanziato solo se non presente nella mappa gestita dalla classe dell'oggetto immutabile. In questo approccio l'identità dei diversi oggetti immutabili è preservata, mettendo in evidenza che l'oggetto richiesto, a parità di token immesso, è condiviso!

Vediamo del codice d'esempio:

  1. public class MyObject 
  2.   private static Set instances = new HashSet(); 
  3.   private final String stringArg; 
  4.   private final Map mapArg; 
  5.  
  6.   private MyObject(String n, Map p) 
  7.   {  
  8.     stringArg = n; 
  9.     mapArg = p;  
  10.  
  11.   public static MyObject get(String data, Map map) 
  12.   {  
  13.     MyObject result = null; 
  14.  
  15.     for (Iterator iter = instances.iterator(); iter.hasNext();) { 
  16.       MyObject tmp = (MyObject)iter.next(); 
  17.  
  18.       if (tmp.stringArg.equals(data) && tmp.mapArg.equals(map)) { 
  19.         result = tmp; 
  20.         break; 
  21.  
  22.     if (result == null) { 
  23.       result = new MyObject(data, map); 
  24.       instances.add(result); 
  25.  
  26.     return result; 
  27.  
  28.   ... 

Un esempio d'uso potrebbe essere il seguente:

  1. String name="Pippo"; 
  2. Map context = new HashMap(); 
  3.  
  4. // Pippo viene creato... 
  5. MyObject first = MyObject.get(name, ctx); 
  6.  
  7. // Pippo viene estratto dall'insieme delle istanze 
  8. MyObject second = MyObject.get(name, ctx); 
  9.  
  10. first == second // TRUE... 

Intrinsic Sharing

In questo approccio l'utilizzatore dell'oggetto immutabile può creare elementi con il classico "new" ma all'interno della classe verrà mantenuto l'insieme degli stati dei differenti oggetti: saranno questi stati ad essere condivisi; da ciò il nome di intrinsic-sharing. Gli oggetti creati hanno, a differenza del primo design, differente identità.

  1. public class MyObject 
  2.   private static Set internals = new HashSet(); 
  3.   private MyObjectInternal internal; 
  4.  
  5.   private static class MyObjectInternal 
  6.     private String stringArg; 
  7.     private Map mapArg; 
  8.  
  9.     MyObjectInternal(String n, Map m) 
  10.       stringArg = n; 
  11.       mapArg = m;     
  12.  
  13.   public MyObject(String data, Map map) 
  14.   {  
  15.     internal = get(data, map); 
  16.  
  17.   private static MyObjectInternal get(String data, Map map) 
  18.   {  
  19.     MyObjectInternal result = null; 
  20.  
  21.     for (Iterator iter = instances.iterator(); iter.hasNext();) 
  22.       MyObjectInternal tmp = (MyObjectInternal)iter.next(); 
  23.  
  24.       if (tmp.stringArg.equals(data) && tmp.mapArg.equals(map)) { 
  25.         result = tmp; 
  26.         break; 
  27.  
  28.     if (result == null) { 
  29.       result = new MyObjectInternal(data, map); 
  30.       instances.add(result); 
  31.  
  32.     return result; 
  33.  
  34.   public String getStringArg() 
  35.     return internal.stringArg; 
  36.  
  37.   ... 

Un esempio d'uso potrebbe essere il seguente:

  1. String name="Pippo"; 
  2. Map context = new HashMap(); 
  3.  
  4. ... 
  5.  
  6. MyObject first = new MyObject(name, context); 
  7. MyObject second = new MyObject(name, context); 
  8.  
  9. first == second  //FALSE 

Conclusioni

Gli oggetti immutabili sono dei tool interessanti anche se in apparenza possono sembrare estremamente limitati. Sono presenti spesso nella programmazione concorrente e in quella, immediatamente affine, ad eventi: un Event Object è frequentemente realizzato come oggetto immutabile posto su un'apposita coda.

Termino questo articolo proponendovi una riflessione sulle possibilità di evidenziare le parti immutabili di un oggetto e con un quesito: Qual è il G.o.F. pattern correlato all'uso della tecnica di extrinsic-sharing?

Informazioni sull'autore

Stefano Fago, classe 1973. Diplomato in ragioneria, ha conseguito il Diploma di Laurea in Informatica con un progetto legato alle interfacce grafiche soft-realtime in Java. Dopo esperienze in Alcatel ed Elea, ha svolto attività di consulenza come Software Developer e Trainer alla ObjectWay S.p.A. sede di Milano. Attualmente Software Designer presso la sezione Innovazione e Attività Progettuali di BPU Banca. Appassionato del linguaggio Java e di tutte le tecnolgie Object Oriented. Polistrumentista dilettante.

È possibile consultare l'elenco degli articoli scritti da Stefano Fago.

Altri articoli sul tema Design / Design Patterns.

Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.