Sei sulla pagina 1di 7

Software Design PROGRAMMING

L’utilizzo di oggetti immutabili in Java si rivela essere un utile tecnica di ottimizzazione in svariati
contesti, oltre ad offrire un concreto utilizzo di meccanismi come il cloning e lo sharing di risorse.

Oggetti immutabili
di Michele Arpaia

Come è stato ben chiarito già ai primordi del movimento dei pattern di design [1], la scelta del
linguaggio di programmazione che si andrà ad utilizzare per “codificare” i pattern è una forza del
contesto che va bilanciata. Come dire, lo stesso pattern ha diverse incarnazioni perché deve dar
conto ai costrutti che il linguaggio offre. Un pattern realizzato mediante un linguaggio di
programmazione prende il nome di idioma. In questa articolo e nel prossimo prenderò in esame un
idioma molto famoso presentando varie tecniche per realizzarlo, tecniche che di volta in volta ne
daranno una caratterizzazione diversa in maniera tale da avere un campo di applicabilità molto
vasto.

Definizione
Un oggetto è detto immutabile se lo stato che lo rappresenta non può essere cambiato dopo la sua
creazione. Un noto esempio di oggetto immutabile è la classe java.lang.String. Una volta
inizializzata la classe String non subirà variazioni di stato. Così ad esempio se String
s=”immutabile”; il metodo seguente non altera il valore originale di s:

public void changeString(String s)


{
s.toUpperCase();
s=s+” o mutabile?”;
}

Al metodo viene passata una copia del reference ad s, ma la classe String non ha metodi che
alterino lo stato interno (mutators methods). Più precisamente i metodi che lavorano sullo stato
della stringa restituiscono un oggetto dello stesso tipo con lo stato aggiornato.
Un altro esempio di oggetti immutabili presenti nelle API Java sono le classi wrapper per i tipi
primitivi.

Figura 1Differenza tra shallow copy e deep copy

Contesto
Generalmente gli oggetti immutabili sono piccoli, hanno poche associazione con altri oggetti e
pongono l’accento più sul contenuto (value based objects) che sull’identità dell’oggetto
consentendo un potenziale resource sharing. Inoltre simulano in maniera semplice il passaggio per
valore, utile in molti casi per migliorare l’occultamento dei dati.

Computer Programming n.139, Ottobre 2004 1


Software Design PROGRAMMING

Un altro campo di applicabilità è dato dagli ambienti multithread ove gli oggetti immutabili sono la
garanzia di thread-safe. Dato che non è possibile cambiare lo stato dell’oggetto, uno o più thread
possono solo leggere lo stato evitando di sincronizzare le risorse con un notevole risparmio e
guadagnando in performance. Come sempre è il progettista che pesa opportunamente la
convenienza o meno di una siffatta soluzione, tenendo anche presente che alcune tecniche per
realizzare l’immutability possono risultare pesanti (come ad esempio il cloning, che vedremo
successivamente).
Un altro vantaggio di avere oggetti immutabili è dato dal loro utilizzo nelle collezioni ordinate, ad
esempio il TreeSet. Se inseriamo oggetti Account in questa collezione (avendo opportunamente
implementato l’interfaccia Comparable) questi saranno mantenuti ordinati secondo il criterio scelto,
che in questo caso semplice potremmo considerare l’ordinamento sul nome. Dato che Account è un
oggetto mutabile, provando a modificare lo stato di un oggetto già inserito nella collection
(attraverso il metodo setNome(), ad esempio), questa ultima non si accorgerà del nuovo stato che
potrebbe richiedere un riordino degli elementi. Il problema si risolverebbe alla radice se Account
fosse immutabile.

Soluzione
Possiamo stendere delle condizioni necessarie per la costruzioni di oggetti affinché questi risultino
immutabili. Queste sono:

• Dichiarazione final della classe


• Definizione private di tutti i data member
• Definizione di soli query methods
• Inizializzazione dei dati nei costruttori

Dichiarare la classe final evita il subclassing e di conseguenza la possibilità di dichiarare metodi


mutators nelle sottoclassi. Non essendoci mutators methods il costruttore è l’unico modo per
inizializzare la classe. I query methods rendono la classe a sola lettura.
Le condizioni sopra elencate rappresentano un insieme necessario poiché una classe pur
rispettandole potrebbe violare l’immutabilità. Ad esempio si consideri il caso di una classe
Resource immutabile (secondo le condizioni sopra citate) che prende come parametro del
costruttore un oggetto mutabile, ad esempio Account. In questo caso chiaramente qualsiasi modifica
all’Account si riflette in una modifica dello stato del Resource. In teoria si potrebbe aggiungere alla
lista delle condizioni quella di avere come parametri di input solo oggetti immutabili, ma questo
restringerebbe di molto il campo di azione di questo idioma, poiché la creazione di un immutabile
costringerebbe le classi utilizzate ad essere ugualmente immutabili. Un altro caso – in verità molto
poco Object Oriented – è la restituzione di oggetti mutabili da parte dei metodi query di una classe.
Questo approccio oltre a violare palesemente l’incapsulamento dei dati (restituisce una parte dello
stato creando un eccessivo coupling con le classi client) è facilmente riconducibile al caso
precedente.

Prima tecnica: Cloning


Le condizioni necessarie sopra citate diventano chiaramente sufficienti quando la classe a sua volta
ha a che fare con oggetti immutabili custom, tipi primitivi e rispettivi wrapper, stringhe, e altri
presenti nelle API Java. Nel caso in cui ci troviamo a lavorare con oggetti mutabili, una tecnica
nativamente presente in Java è il cloning. Si tratta semplicemente di clonare gli oggetti passati al
costruttore e quelli restituiti dai metodi getter. Riconsideriamo le classi Resource e Account, ove
Resource è immutabile mentre Account non lo è.

public final class Resource


{

Computer Programming n.139, Ottobre 2004 2


Software Design PROGRAMMING

public Resource (String nome, Account acc)


{ …}
// altri ctor
public Account getAccount()
{ …
return acc;
}
//altri metodi getter

}

public class Account


{
public Account (String nome)
{…}
// altri ctor
// altri metodi getter/setter

}

Un frammento di codice che utilizza queste classi potrebbe essere:

Account acc = new Account (“Administrator”);


Resource res = new Resource (“PrinterHP”, acc);

acc.setName(“User”);

E’ chiaro che Resource pur costruita per essere immutabile di fatto si trova a diventare mutabile a
causa di Account. Il problema è che Resource riceve in input la copia del reference all’oggetto
Account e non la copia dell’oggetto. Giacché ci siamo, è utile notare che neppure qualificando final
il parametro Account del costruttore di Resource risolverebbe il problema.
Il cloning ci viene di aiuto e dato che ogni buon sviluppatore Java conosce bene la differenza tra
shallow copy e deep copy tratterò questi due casi separatamente senza soffermarmi troppo sul loro
significato (si veda comunque la Figura 1)

Shallow Copy
Riprendiamo la classe Resource e modifichiamo il costruttore come segue:

public Resource (String nome, Account acc)


{

this.acc = (Account) acc.clone();
}

public Account getAccount()


{

return acc.clone();
}

Computer Programming n.139, Ottobre 2004 3


Software Design PROGRAMMING

La “nativa” shallow copy effettua una copia bit a bit dei campi dell’oggetto in esame. Naturalmente
per sfruttare questo meccanismo nativo dobbiamo rendere Cloneable la classe Account e
implementare il metodo clone come segue:

public Object clone(){


try {
return super.clone();
}
catch (CloneNotSupportedException e) {
//This should not happen, since this class is Cloneable.
throw new InternalError();
}
}

Stiamo assumendo che la classe Account utilizzi (per il suo stato) oggetti tutti clonabili.
Riprenderemo questo punto più avanti quando parleremo del deep copy. In alcuni casi potrebbe
essere più pratico mettere a fattor comune il metodo clone() in una apposita classe astratta per poi
estenderla.
Prima di chiudere con questa tecnica credo sia utile richiamare un altro aspetto spesso sottovalutato
e causa di sottili errori. Consideriamo di aggiungere alla classe Resource un ulteriore costruttore che
prenda in input un insieme di account ad esso associato (naturalmente esisterà il metodo
getAccounts() che restituisce la lista degli account associati). Potremmo facilmente utilizzare un
ArrayList, dunque:

public Resource (String nome, ArrayList accs)


{
this.nome=nome;
this.accs=(ArrayList)accs.clone();
}

Un frammento di codice che utilizza queste classi potrebbe essere:

Account acc1 = new Account ("Administrator");


Account acc2 = new Account ("Guest");
ArrayList accs = new ArrayList();
accs.add(acc1);
accs.add(acc2);
Resource res = new Resource ("PrinterHP",accs);

ArrayList accs_ = res.getAccounts();


System.out.println("Resource "+res.getNome()+ " belongs to: \n" +
(Account)accs_.get(0) +"\n"+ (Account)accs_.get(1));
acc1.setName("User");
System.out.println("Resource "+res.getNome()+ " belongs to: \n"+
(Account)accs_.get(0)+ "\n"+ (Account)accs_.get(1));

L’output risulta:

Resource PrinterHP belongs to:


Administrator
Guest
Resource PrinterHP belongs to:
Computer Programming n.139, Ottobre 2004 4
Software Design PROGRAMMING

User
Guest

Non proprio secondo le nostre aspettative. Cosa è successo? Semplicemente il clone di ArrayList
funziona secondo shallow copy dunque è stata fatta una copia dei reference degli oggetti contenuti
(questo vale anche per gli array). Nel prossimo paragrafo risolveremo il problema attraverso il deep
copy.

Deep Cloning
Un modo semplice per risolvere il problema precedente è quello di utilizzare l’ereditarietà. Si
costruisce una nuova classe derivata da ArrayList e si implementa il metodo clone che costruisce
una nuova lista di elementi clonati. E’ un approccio poco percorribile (esempio di cattivo uso di
ereditarietà) in quanto costringe a creare una classe artificiale utile solo per il cloning. Inoltre la
classe Resource cambia interfaccia e questo impatta su tutti i client (violazione del Open/Closed
Principle), e infine la sottoclasse non è sostituibile nei contesti in cui appare la superclasse poiché
aggiunge metodi ad hoc.
Un approccio migliore è quello del deep coping sulla classe Resource. Quando questa prende in
input (dai costruttori) il parametro ArrayListt, si preoccupa di farne una copia “profonda” in un
metodo private. Questo metodo potrebbe avere la seguente forma:

private ArrayList clone(ArrayList al)


{
int size = al.size();
ArrayList newAL = new ArrayList (size);
for (int i=0; i<size; i++)
newAL.add(((Account)al.get(i)).clone());
return newAL;
}

Questo metodo è invocato al momento della costruzione della classe Resource e nei metodi getter.
Da notare che la classe Account potrebbe aver implementato un deep copy.

Considerazioni sul cloning


La tecnica presa in esame aggiunge la condizione sufficiente per raggiungere una piena
immutabilità degli oggetti con tutti i vantaggi che questo comporta una volta appurata l’opportunità
del suo utilizzo. Un rischio associato a questa tecnica è quella del cosiddetto over-cloning. Dato che
non ci sono vincoli espliciti, i client dell’oggetto immutabile by cloning potrebbero effettuare una
copia dell’oggetto prima di chiamare il costruttore, ad esempio un generico client potrebbe clonare
la classe Account per poi passare questa copia al costruttore di Resource con un chiaro dispendio di
risorse.

Seconda tecnica: Read-Only Interface


Molto spesso ci troviamo ad operare su codice già scritto e a fronte di nuovi requisiti vorremmo
caratterizzare alcuni oggetti trasformandoli in immutabili. Potremmo naturalmente mettere mano al
codice e applicare la tecnica di cloning vista precedentemente. Un'altra tecnica (più debole del
cloning) è quella chiamata Read-Only Interface (d’ora in poi ROI). Si tratta in pratica di costruire
interfacce a sola lettura (solo metodi getter) sulle esistenti classi che vogliamo trasformare. Ad
esempio avendo la classe Account mutabile potremmo ricavarne una versione immutabile attraverso
le seguenti operazioni:

1. Definire una interfaccia con tutti i query metodi pubblici della classe Account

Computer Programming n.139, Ottobre 2004 5


Software Design PROGRAMMING

2. Far implementare tale interfaccia ad Account


3. Sostituire tutti i riferimenti a Account in ImmutabileAccount (se questo è il nome dato
all’interfaccia)

Il punto tre va applicato con molta cautela poiché non stiamo trasformando una classe da mutabile
ad immutabile, stiamo semplicemente aggiungendo una vista immutabile a tale classe. Quindi
questo significa che all’interno del sistema ci saranno parti di codice che utilizzano Resource come
mutabile ed altri che necessitano ora di averla come immutabile. Ovviamente è questo ultimo caso
da prendere in considerazioni nella sostituzione dei riferimenti suggeriti dal passo tre.
Riconsideriamo l’esempio visto sopra e applichiamo questa tecnica in luogo del cloning.
L’interfaccia sarà così definita:

public interface ImmutabileAccount


{
void String getName();
}

Applicando il passo tre nella classe Resource modifichiamo il costruttore in questo modo:

public Resource (String nome, ImmutabileAccount acc)


{
this.nome=nome;
this.acc=acc;
}

Ora Resource - che era già immutabile – conserva questa caratteristica poiché lavora solo con la
versione immutabile di Account. Un tipico esempio di utilizzo potrebbe essere:

ImmutabileAccount acc = new Account(“Administrator”);


Resource res = new Resource (“PrinterHP”, acc);
acc.setName(“User”); // compile time error!

Come si vede il compilatore effettua un controllo statico evitando l’invocazione di setName()


poiché questo non appare dichiarato nell’interfaccia ImmutabileAccount.
Prima ho affermato che questa tecnica è più debole del cloning poiché risulta facilmente
“aggirabile”. Infatti possiamo sempre scrivere:

(Account)acc.setName(“User”);

che viola palesemente il principio di immutabilità che si vuole raggiungere. Resta comunque utile
poiché non introduce – come il cloning – un eccessivo overhead ed è molto più semplice da
realizzare aiutando anche alla costruzione di qualche astrazione eventualmente utile al design. Si
rivela particolarmente pratica nel caso dei metodi query che in questo caso, restituendo una versione
immutabile degli oggetti che rappresentano il proprio stato, aiuta a raggiungere un buon grado di
“information hiding”[2]

Considerazioni su ROI
Questa tecnica si offre a molte utili estensioni. Un enhancement è quello di combinare questa
tecnica con quella del cloning. E’ sufficiente aggiungere nell’interfaccia ROI la firma del metodo
clone ed obbligare la sottoclasse ad implementarlo. In questo modo si ha la libertà di scegliere se un
oggetto passato immutabile ci serve comunque utilizzarlo come mutabile.

Computer Programming n.139, Ottobre 2004 6


Software Design PROGRAMMING

In alcuni casi risulta più conveniente affiancare all’oggetto immutabile uno mutabile, avere così
due versioni concrete di una stessa entità.Un modo elegante è quello di definire una classe che
implementa il comportamento standard, quindi mutabile. Questo oggetto delega il comportamento
dei metodi query alla classe immutabile tramite l’interfaccia ROI. Ad esempio, potrebbe esistere
l’interfaccia ROI che chiamiamo ImmutabileAccount implementata da Account, cosicché questa
classe è immutabile per definizione. Volendo definire un account mutabile, si costruisce una classe
AccountMutable che delega, in questo semplice caso, l’esecuzione del metodo getName() alla classe
immutabile tramite l’interfaccia ROI. Naturalmente questo tipo di associazione potrebbe
completamente non esistere (si pensi a String e StringBuffer). Ad ogni modo abbiamo due reali
versioni di un oggetto e questo evita il downcasting che era il tallone di Achille della tecnica ROI.
Si noti che lo stesso risultato si raggiunge attraverso l’ereditarietà, definendo cioè le due classi
(mutabile ed immutabile) come sottoclassi di ROI. Lascio al lettore l’esplorazione di questi casi.
.

Conclusioni
Le due tecniche esaminate in questo articolo consentono di costruire e/o trattare con oggetti
immutabili laddove questo si rende necessario. Si è accennato ad un potenziale utilizzo del resource
sharing, tecnica utilizzata anche dalla classe String. Nel prossimo articolo vedremo come gli oggetti
immutabili siano i naturali candidati per il resource sharing mostrando le due principali tecniche
associate.

Bibliografia
[1] Gamma E., Helm R., Johnson R. & Vlissides J., Design Patterns: Elements of Reusable
Design, Addison-Wesley,1995
[2] Holub Allen – “Why getter and setter methods are evil”, JavaWorld, Settembre 2003

Michele Arpaia, laureato in Scienze dell’informazione, attualmente lavora come Team Leader per
Accenture Technology Solutions. Svolge soprattutto attività di mentoring, recuitment, gestione
progetti e supporto tecnico alle fasi di analisi, design ed implementazione su tecnologia Java e
C++.

Computer Programming n.139, Ottobre 2004 7

Potrebbero piacerti anche