Articoli Manifesto Tools Links Canali Libri Contatti ?
Linguaggi / Java

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
di Stefano Fago Discuti sul forum   Stampa

Ancora una volta Iterator!

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:
anotherMethod()
{
  ...
  try {
    int sum = computeTotalAmount(bag);
  } catch (ProcessException pexc) {
    // handling illegal ShoppingBag ...
  } catch (RuntimeException rexc) {
    // try to handle problematic situation ...
  } 
  ...
}

Un prima tabella di riferimento.

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.

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

Altri articoli sul tema Linguaggi / Java.

Risorse

  1. Parte 1: Eccezioni Java: un esperimento mal riuscito?
    http://www.siforge.org/articles/2002/09/01-eccezioni_java.html
  2. Parte 3: Eccezioni e Object Orientation in pratica
    http://www.siforge.org/articles/2002/12/04-eccezioni_java-3.html
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

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