Eccezioni Java #2: eccezioni o non eccezioni? Questo non è un dilemma!
Abstract
In questa seconda parte finiremo la discussione sulle eccezioni negli iteratori che
ci porterà ad una nuova regola necessaria ad ampliare la nostra visione della
gestione degli errori in Java.
Data di stesura: 21/10/2002
Data di pubblicazione:
23/10/2002
Ultima modifica: 04/04/2006
Esaminando l'interfaccia java.util.Iterator abbiamo individuato alcuni elementi
fondamentali nell'uso delle eccezioni : tra questi vi è il concetto di progettazione
per contratto che risulta estremamente utile e implicitamente supportata dalla
logica delle exception di Java. Quello che si vuol evidenziare è che il design di
un'astrazione comprende un attento studio del ruolo e responsabilità ad essa
attribuite portando al meccanismo delle eccezioni lì dove non vengano rispettati i
vincoli emersi in ambito progettuale. Conferma a quanto già esposto è riscontrabile
nel metodo remove(). Scorrendo le A.P.I. Java troviamo, infatti, che il metodo in
questione può attuare il throwing di due tipi di eccezioni:
java.lang.IllegalStateException e java.lang.UnsupportedOperationException.
Ricordiamo gli elementi guida emersi nel primo articolo: "Se non vengono
rispettate le precondizioni di un metodo si dovrebbe lanciare un'eccezione", "Se
un metodo incontra una condizione anormale che non può gestire dovrebbe
lanciare un'eccezione". La java.lang.IllegalStateException è legata alla
semantica dell'iteratore: il contratto di questa astrazione prevede che l'operazione
di remove sia possibile solo se preceduta da una invocazione al metodo next(); la
coppia di operazioni (remove,remove) rappresenta quindi una violazione che deve
essere segnalata. L'eccezione UnsupportedOperationException viene lanciata
allorchè l'operazione di remove() risulti non implementata dal dato iteratore.
Questa potrebbe sembrare una situazione atipica: perchè un'iteratore non
dovrebbe supportare la possibilità di modificare la collezione ad esso sottesa? Un
elemento di design che risponde a questo requisito può essere un elenco costante
(Singleton) o una lista non modificabile (Unmodifiable Element): considerando la
classe java.util.Collections troviamo, infatti, che è possibile la creazione di più di
un insieme o mappa non alterabili! Un'altra situazione in cui si riscontrano
unmodifiable collection è quella espressa dal metodo asList() della classe
java.util.Arrays che trasforma un array di oggetti in una implementazione
dell'interfaccia java.util.List. Allorchè si usi l'iteratore della lista così ottenuta,
scopriamo che non è possibile sfruttare il metodo di remove(), il che ci viene
segnalato dall'apposita eccezione! Il caso della UnsupportedOperationException
ci fornisce il giusto pretesto per evidenziare il fatto che un'eccezione non è
necessariamente un errore: il metodo remove() è tipico di un iteratore ma, la
possibilità che non venga implementato è un evento eccezionale che può
presentarsi, non un errore o violazione di qualche contratto!
Usare i flag? Una nuova regola...
Abbiamo visto, già nel primo articolo, che a volte le eccezioni possono essere
evitate se l'astrazione ci fornisce metodi alternativi per la verifica di un qualche
elemento di stato. Questa visione delle cose non deve essere fraintesa e riportare al
preconcetto che le eccezioni sono un elemento fastidioso e poco utile: come
distinguere, allora, i momenti in cui applicare le eccezioni o usare i flag?
È necessario basarsi solo su considerazioni legate alle performance? La
problematica, in questione, non ha sempre una risposta corretta ed a volte è
l'esperienza a dettare la giusta soluzione.
Nell'analisi che abbiamo fatto degli iteratori Java vi è una linea guida che può
essere riassunta nella seguente regola: "Se il contesto d'uso di un metodo è locale,
generalmente, non conviene usare le eccezioni: è possibile adottare algoritmi di
verifica efficienti ed è possibile utilizzare la politica dei valori speciali di
ritorno". Estendiamo il concetto esposto e per far ciò scomponiamo la nostra
regola in due punti: la problematica delle performance e il concetto di contesto
locale.
La caratteristica di un iteratore è di permettere l'analisi della struttura dati a cui si
riferisce e a volte anche la sua modifica. Questo implica che un'istanza di iteratore
viene fruttata ripetutamente e dev'essere sufficientemente leggera da non
vincolare l'attività dello sviluppatore: un Iterator è spesso usato nel corpo
implementativo di un metodo e, con il semplice pattern di utilizzo previsto, non si
appesantisce la scrittura del metodo stesso! Usare un iteratore è effettivamente
semplice e difficilmente crea problemi in fase di sviluppo, così come difficilmente
ci appaiono eccezioni in fase esecutiva: le performance, e la produttività dello
sviluppatore, sono salve grazie all'uso di flag. È anche vero che per evitare
inconsistenze nella struttura dati sottesa all'iteratore e per eventuali azioni di
recovery su di essa i flag sarebbero inopportuni: le eccezioni, ed i meccanismi ad
esse legati, ci permettono di intervenire in situazioni critiche, inaspettate o di
violazione di contratto in modo elegante, pulito e solo quando effettivamente
richiesto!
Il termine locale generalmente implica una visibilità limitata degli elementi: nella
nostra discussione intenderemo per locale elementi la cui visibilità è di tipo
private. Una buona pratica nello sviluppo è lo scomporre un'operazione di
dimensioni sensibili i porzioni di codice minori e possibilmente riusabili. Nel
mondo ad oggetti questa idea rimane valida e trova una nuova applicazione dovuta
al concetto di incapsulamento. Un'astrazione oltre agli elementi pubblicati al
mondo esterno, ne presenta altri privati, comprendendo anche operazioni per la
gestione dello stato interno, che non devono essere presentati al di fuori dei confini
del modulo. Possiamo pensare, quindi, che operazioni pubbliche vengano in realtà
implementate dalla composizione di operazioni private. Con quest'ottica risulta
facile capire che sfruttare eccezioni nei metodi privati può risultare ridondante e a
volte deleterio, specie per le performance.
Le eccezioni serviranno nell'interfaccia pubblica di una data astrazione per
segnalare oltre alla violazione di un dato contratto, anche un malfunzionamento
inatteso eventualmente scatenato da un elemento privato.
Vediamo, ora, un esempio programmativo per riassumere ed applicare quanto
espresso nei due articoli pubblicati.
Supponiamo di sviluppare un sito di commercio elettronico per un negozio di libri.
Nella politica commerciale della maggior parte dei negozi vi è l'applicazione di
sconti sugli acquisti, differenziati in base alla tipologia di cliente. Scriveremo, tra
le altre, una classe di utilità onde permettere il calcolo della somma che deve
essere pagata per i libri presenti nel carrello della spesa.
Possiamo ipotizzare un'interfaccia pubblica che comprenda un metodo scritto
come segue:
public int computeTotalAmount(ShoppingBag bag) throws ProcessException
{
if (bag == null)
throw ProcessException("Passing invalid bag to process!");
int userId = bag.getUserId();
Book books = bag.getBooks();
Profile profile = findUserProfile(userId);
if (profile == Profile.UNKNOWN_USER)
return normalSum(books);
return computeSum(profile, books);
}
Come prima cosa il metodo controlla che l'attuale carrello sia un elemento valido.
La validità della ShoppingBag è una pre-condizione necessaria perchè
l'esecuzione dell'operazione vada a buon fine: se c'è violazione viene lanciata
un'eccezione. Dal carrello della spesa si deduce il codice dell'acquirente e l'elenco
dei libri da questi scelti. A questo punto si individua che tipologia di cliente
corrisponde all'attuale soggetto. Il metodo privato findUserProfile()
probabilmente itera su una quantità sensibile di elementi ma per segnalare un
eventuale fallimento della ricerca non attua il throwing di un'eccezione bensì
ritorna un valore speciale.
Il metodo computeSum() prevede un profilo valido per le sue operazioni e, quindi,
fornire un profilo errato vuol dire violazione di contratto e conseguente presenza
di un'eccezione! Questo implicherebbe codice nella forma:
...
int sum = 0;
try {
sum = computeSum(profile, books);
} catch (InvalidProfileException ipexc) {
// alternative action to manage sum!
}
...
Pur essendo corretto il codice proposto , in presenza di un profilo sbagliato,
l'azione di recovery può essere costosa. Come nel caso dell'iteratore, però, adottare
la politica del valore speciale ci permette un'elaborazione sicura senza essere
penalizzati nelle performance.
Nuove potenzialità: i tipi di eccezioni.
I programmatori Java sanno che il mondo delle eccezioni è spezzato in due grandi
famiglie: le checked exception e le unchecked exception. Le prime sono controllate
dal compilatore ed obbligano l'uso della clausola throws o degli elementi
try/catch. Le seconde non implicano alcun elemento aggiuntivo e, quindi,
verranno propagate nello stack delle chiamate fino al momento in cui il processo
termina o un dato catcher gestisce le eccezioni. Questa differenziazione è
importante è permette allo sviluppatore di determinare un design evoluto per la
gestione degli errori nei propri artefatti. Riprendiamo il codice di esempio.
Notiamo che il metodo computeTotalAmount() dichiara di lanciare un'eccezione
checked, costringendo chi sfrutterà tale metodo al catch della exception o alla sua
propagazione con un ulteriore throw. Questo modo di agire si rende necessario
perchè la presenza di una ShoppingBag semanticamente inopportuna impedisce
una corretta elaborazione. Una checked exception forza lo sviluppatore a
considerare degli errori per cui è possibile attuare un'azione di recovery
evitando di lasciare l'ambiente esecutivo o delle risorse importanti in uno stato
inconsistente. Consideriamo ora il metodo findUserProfile().
In quanto locale è stato deciso di usare la politica dei valori speciali onde evitare la
presenza di eccezioni che appesantissero l'uso del metodo o che determinassero il
degrado delle performance. In realtà questo non basta! Il metodo in questione
potrebbe usare una connessione verso un database remoto per attuare la propria
elaborazione: situazioni di errore sul DB devono essere segnalate e non possono
essere riportate come particolare flag. In queste condizioni le unchecked exception
risultano utili: il metodo findUserProfile() potrà lanciare una RuntimeException
che non vincola lo sviluppatore ma, che permetterà comunque di segnalare
una situazione inaspettata, di cui non si sa attuare il recovery!
Da un punto di vista pratico il metodo computeTotalAmount() verrà usato in una
modalità operativa nel forma:
Sapendo di far cosa gradita a chi ha voluto seguirmi sino a questo punto presento
una tabella di riferimento per riepilogare le scelte nell'uso delle eccezioni, secondo
quanto sino ad ora esposto.
Violazione contratto di un'astrazione
uso eccezione.
Stato inaspettato
uso eccezione.
Metodo privato
uso flag o metodo per controllo stato.
Metodo da usare in loop
uso flag o metodo per controllo stato.
Metodo pubblico
uso eccezioni.
Violazione contratto di un'astrazione
uso unchecked exception.
Stato inaspettato
uso unchecked exception.
Metodo privato
preferibilmente non uso eccezioni, ma
se devo segnalare problemi uso
unchecked exception.
Metodo da usare in loop
preferibilmente non uso eccezioni, ma
se devo segnalare problemi uso
unchecked exception.
Metodi pubblici
uso checked exception per segnalare
allo sviluppatore la possibilità che si
presentino situazioni di cui fare il
recovery. Posso usare anche eccezioni
unchecked per segnalare elementi
inaspettati e violazioni di contratto; in
questa ipotesi sarebbe buona prassi
segnalare nella clausola di throws anche
le unchecked exception utilizzate.
Conclusioni
Nel prossimo ed ultimo articolo presenteremo una serie di strategie programmative
legate all'uso delle eccezioni, facendo notare come anche per le eccezioni è
importante un design ad oggetti ed ancora prima uno studio architetturale.
Potrebbe sembrare strano parlare di architettura su elementi puramente
programmativi ma, in realtà, vedremo come la gestione degli errori deve essere
inclusa nello sviluppo verso il design dell'architettura scelta per il dato progetto.
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.