Eccezioni Java: piccolo riepilogo
Il linguaggio Java ha tre categorie di eccezioni:
-
Istanze della classe
java.lang.Exception
e sue sottoclassi;
-
Istanze della classe
java.lang.RuntimeException
e sue sottoclassi;
-
Istanze della classe
java.lang.Error
e sue sottoclassi.
Il compilatore Java considera la prima famiglia di eccezioni, dette checked, in modo differente dalle altre due, dette unchecked. Quando un metodo crea una checked exception a ne attua il throw, il compilatore si aspetta che nel corpo del metodo ci sia una forma try/catch o che, nella signature del metodo, ci sia una clausola di throws dello stesso tipo di eccezione lanciata o di una sua superclasse. Se nessuna delle condizioni ora esposte è riscontrata si incorre in un errore di compilazione!
Le eccezioni unchecked sono libere da costrizioni programmative: esse possono essere lanciate ovunque e, se non gestite, si propagano nello stack delle chiamate fino a causare la terminazione del programma.
Checked & Unchecked: un vero problema?
La presenza di due tipologie di eccezioni può confondere lo sviluppatore meno esperto: nelle A.P.I. Java sono presenti numerose checked exception che condizionano la produttività del singolo e che portano a stili programmativi non opportuni o, a volte, deleteri come nel caso dei catcher generici e delle forme try/catch vuote.
La Microsoft nel suo ultimo linguaggio, il C#, seguendo una tendenza di mercato, non ha incluso checked exception visto che la produttività è diventata una qualità sempre più richiesta nello sviluppo di software. Nel mondo Java, guru, come Bruce Eckel, hanno allestito dei forum sull'utilità delle checked exception e loro eventuale soppressione dal linguaggio a favore delle più "produttive" unchecked exception.
In realtà il problema è da suddividersi in due parti: il rapporto produttività/checked exception e l'uso delle unchecked exception.
Per quanto riguarda il secondo punto, nessuno vieta l'uso delle eccezioni di runtime nei propri artefatti. È necessario però farlo con criterio: in quest'ottica un primo aiuto ce lo forniscono le tabelle presentate nel precedente articolo.
Dovremo considerare inoltre la stratificazione del nostro prodotto per riuscire a capire quando lanciare un'eccezione, quando attuarne il catching e quando mascherare l'errore in un flag o in un'altra eccezione. Questo discorso può sembrare banale ma, su progetti di grandi dimensioni, lo sforzo è sensibile.
Per la prima volta emerge come la gestione delle eccezioni ha un risvolto architetturale dato che, per usare bene le unchecked exception, dobbiamo studiare, prima, "cosa" deve svolgersi e, poi, "come" attuarlo!
Supponiamo di sviluppare un applicativo in cui venga utilizzato un database ed anche delle interfacce grafiche come quelle ottenibili con il toolkit Swing. È possibile individuare una semplice architettura stratificata in cui il DB è il layer di base, su cui si appoggiano in livelli successivi, la logica del prodotto e l'implementazione della GUI.
Figura 1: Layer Architetturali
In questa situazione ci risulta più evidente che per l'utente finale è incomprensibile una SQLException scatenata dal DB; sarà necessario ottenere un errore significativo e quindi adeguato al livello di astrazione percepito dallo user. Discorso analogo vale per il layer di "logica applicativa". Se ne deduce che, per ogni strato architetturale, sono necessarie una o più famiglie di eccezioni proprie del dato layer: il passaggio da un livello ad un altro vuole un'opera di traduzione ed adeguamento che porterà alla trasformazione di eccezioni in altre tipologie di eccezioni o in flag opportuni.
Sarà altresì necessario capire quando scatenare un'eccezione e quando trattarla: la validazione di base dei dati immessi da un utente non deve avvenire nel layer del DB ma attuarsi già nel livello della GUI, a confine con la logica applicativa. Al layer intermedio spetterà il controllo di errori legati ad una seconda fase di validazione. Per il nostro semplice esempio potremmo vedere un stratificazione altrettanto semplice delle responsabilità nella gestione delle eccezioni:
-
layer GUI: eccezioni del toolkit, eccezioni di validazione (base), warning come traduzione di eccezioni provenienti da altri livelli;
-
layer LOGICA APPLICATIVA: eccezioni di elaborazione, eccezioni di validazione (avanzate), warning come traduzione di eccezioni provenienti da altri livelli;
-
layer DB: errori di elaborazione e comunicazione con il database.
A questo punto notiamo che le considerazioni fatte possono essere tranquillamente applicate ad un ambiente che presenti le checked exception; ancora di più risulta interessante come lo studio architetturale, pur coinvolgendo la gestione delle eccezioni, non è condizionato dalla loro tipologia.
Le difficoltà introdotte dalla presenza di checked exception sembrerebbero dichiararne l'inutilità o la loro rappresentazione come un esperimento mal riuscito; ma è proprio così? Come spesso accade la verità è nel mezzo! Sicuramente qualsiasi sviluppatore Java si è scontrato con l'eccessivo carico dettato dalle IOException o SQLException o altre checked exception disseminate sensibilmente nelle A.P.I. Java; questo non significa che questi oggetti sono inutili! Vediamo alcuni perché:
-
Le checked exception dovrebbero essere usate per circostanze eccezionali in cui sia possibile attuare il recovery del problema o dello stato dell'applicativo in generale. Queste condizioni sono generalmente un insieme limitato e ben definibile. Le checked exception hanno quindi un ruolo preciso attribuibile programmativamente ad ogni loro aspetto: questo ruolo non può essere svolto da runtime exception!
-
La clausola di throws, legata alle checked exception, ha un'importanza nel design e nella manutenzione di un artefatto. Ad esempio, se un metodo non elenca tutte le eccezioni che può lanciare, non solo non è chiaro il contratto del metodo ma, in mancanza di codice sorgente, è difficile individuare quale errore può sorgere nel prodotto.
-
Ciò che può risultare un errore di runtime ad un dato livello di astrazione può diventare un elemento trattabile e su cui intervenire ad un altro livello di astrazione: anche in un'azione di traduzione la presenza di checked exception offre un'alternativa utile per aumentare la flessibilità del design!
Il problema della produttività e delle checked exception è quindi dettato dall'inesperienza, dalla indisciplina dello sviluppatore e dalla stessa fretta produttiva troppo spesso indicata come bisogno!
Strategie per la gestione di eccezioni
Vediamo adesso delle strategie programmative per l'uso di eccezioni. Gli elementi presentati non sono sempre da applicarsi contemporaneamente; a volte è opportuno scegliere tra alternative differenti in base alle necessità architetturali e di design.
STRATEGIA 1: Usare unchecked exception per problemi di logica programmativa.
L'eccezione utilizzata deve presentare il suffisso RuntimeException. Ad esempio la presenza di un parametro nullo può essere segnalata con una NullParameterRuntimeException.
STRATEGIA 2: Creare classi di eccezioni per esprimere "cosa" non è andato a buon fine e non "dove" si è verificato l'errore.
L'idea è quella di evitare il proliferare delle eccezioni: nel nome degli oggetti deve essere espresso il problema e non dove questo si sia verificato. Ad esempio invece che scrivere una UserDBException è meglio creare una QueryWhereClauseException.
STRATEGIA 3: Usare uno stesso template per i messaggi di errore di una data famiglia di eccezioni.
In questo caso ci si riferisce alla necessità di avere messaggi di errori che non siano hard-coded ma studiati per promuovere il debugging e l'internazionalizzazione del codice.
STRATEGIA 4: Creare una superclasse astratta per un insieme di eccezioni correlate.
Esempio di tale strategia sono le IOException: vi sono molte specializzazioni di questa root-class ma, i metodo principali delle differenti astrazioni presentano la clausola di throws legata all'elemento più generico l'IOException. Con questa politica si attua un design ad oggetti anche per le eccezioni e si promuove il riuso grazie all'approccio incrementale possibile con l'ereditarietà. Un ulteriore vantaggio consiste nell'evitare modifiche a catena nel caso che un metodo debba lanciare un'eccezione differente da quella inizialmente prevista o nel caso venga creata una nuova tipologia di eccezione per un dato problema.
STRATEGIA 5: Non propagare eccezioni da uno strato architeturale ad un altro.
È buona prassi che uno strato architetturale non attui il catch di eccezioni emerse dai livelli sottostanti: oltre alla pulizia concettuale , non è detto che in dato layer vi sia la possibilità di attuare il recovery di un problema. In generale è da adottare un meccanismo di traduzione tra i vari layer che preservi le informazioni raccolte e che adegui le eccezioni al livello di astrazione in cui giungono.
STRATEGIA 6: Non attuare il catch delle eccezioni con azioni vuote.
Una cattiva prassi è quella di attuare il catch di eccezioni solo per eliminare il carico nella scrittura del codice dovuto alle forme try/catch. Nei casi in cui è necessario ricondursi a questa politica si consiglia di attuare un log di quanto eventualmente processato nel blocco catch. Politiche più complesse, a volte non pulite dal punto di vista del design, potrebbero prevedere l'uso di flag globali o gestori di alto livello.
Conclusioni
L'uso delle eccezioni Java e la gestione degli errori è, comunque, una problematica non semplice che non deve essere sottovalutata, specie in progetti di dimensioni sensibili. In un mondo ad oggetti le eccezioni sono al pari di altri elementi e come tali dovranno sempre essere considerati e trattati; tale idea viene ad essere ulteriormente rafforzata dalla visione che le eccezioni hanno un ruolo importante anche nelle fasi di testing e debugging.
Gli articoli proposti hanno cercato di evidenziare tecniche ed elementi da usare per facilitare l'attività dello sviluppatore che deve comunque seguire una disciplina programmativa opportuna oltre che "il caro e vecchio" buon senso!