Checked Exceptions, un’alternativa notturna

Fino a pochi giorni fa sulla mailing list del jug Torino si discuteva dell’utilità delle checked exceptions.

Antonio le odia profondamente e in un certo senso anch’io, tra di noi resta un piccolo differenziale : io dico “sono così odiate perchè sono abusate, basterebbe che non fossero la scelta di default quando si parla di eccezioni” e lui dice “visto che dovrebbero essere usate di raro e invece vengono messe come il prezzemolo togliamole e basta”. Penso di poter dire che siamo daccordo sul problema, ma che proponiamo diverse strategie correttive, che non voglio discutere qui.

Questo post lo sto scrivendo perchè circa quaranta minuti fa erano le due di notte e stavo tentando di addormentarmi, erano già due ore che ci provavo rileggendomi il primo libro di Harry Potter e, a testimonianza del valore del testo, non era servito a nulla.

Ho chiuso il libro e spento la luce e ho cercato di pensare a qualcosa per rilassarmi. Strano a dirsi, ma mettermi a pensare alle discussioni sulle checked exceptions del jug non ha aiutato per niente e invece ho iniziato a formulare un’idea di cui al momento non mi azzardo a stimare il valore, vista l’ora e il mio oggettivo stato di dormiveglia, ma che prevedo che all’alba mi suonerà bislacca e scialba di contenuto.

Mi impegno comunque a metterla online domattina, se non altro per il suo valore di divertissement, non appena potrò permettermi di accendere il computer che è connesso a internet, cosa che, se mi azzardassi di farla adesso, scatenarebbe le giuste e funeste ire della mia dolce metà.

Passo quindi ad illustrare l’idea bislacca, premettendo ancora una cosa : non ho idea se quello che sto per descrivere è cosa già ben nota o se addirittura fa parte della tradizione, se lo è non servirà certo arrivare alla fine perchè suoni il campanello a chi già sa e quindi il danno sarà minimo, ad ogni modo le mie uniche scuse saranno la mia ignoranza e l’ora improponibile.

La cosa che veramente mi fa dare di testa con le checked exceptions, non solo quando si tratta di eccezioni che dovrebbero invece essere di runtime (e qui l’abuso di cui sopra), ma anche quando avrebbero il loro perchè, è che spronano alla duplicazione del codice.

Un try/catch è di per sè uno switch su due o più casi e se c’è una caratteristica degli switch/if è che non permettono il riuso del codice di gestione all’interno dei loro casi.

public void chiamante() {

try {

chiamato();

} catch (Exception e) {

}

}

Certo, al ripetersi di una condizione è possibile estrarre un metodo e richiamarlo ogni volta, ma, a meno di non abbarbicarsi con qualcosa di statico, il riuso finisce lì, spesso limitato a una singola classe, magari persino esteso a uno Strategy che gestisca gli errori in molti diversi oggetti, comunque di solito non si riesce a liberarsi dello switch in sè, si riusano solo i singoli casi, il riuso non è mai davvero trasparente.

Quante volte si vedono dei catch duplicati in mille punti diversi del codice e si ci sente nel bisogno di riconoscervi un concern trasversale che richiederebbe davvero la programmazione ad aspetti, pena il copia incolla e quindi l’esplosione della manutenzione.

Spesso sono proprio le eccezioni che non dovrebbero essere checked che generano la duplicazione più diffusa. Non c’è niente di più facile che copiare un printStackTrace o un bel log.error(e), e questi spesso indicano delle false checked : se non sappiamo come gestirla perchè mai dovremmo essere obbligati a farlo in questo modo che, alla meglio, introduce un bel fail-slow?

Questa notte stavo ripensando a questa duplicazione e l’ho collegata alla discussione sul “contratto” del metodo e di come questo sia ulteriromente specificato dalla presenza di un’eccezione checked o unchecked. Forse il motivo per cui questo post mi diverte tanto e non mi fa dormire è che nasce da una considerazione del tutto teorica, che non ha nulla di pratico in partenza, ma in cui trovo un deciso valore pratico.

Si può vedere un try/throws e quindi la dichiarazione di una checked exception da parte del metodo come la richiesta del rispetto di una debole PREcondizione : “per potermi chiamare devi fornire una try con la catch giusta o al massimo una throws”. Mentre nel caso delle eccezioni unchecked si tratta di una debole postcondizione “se succede qualcosa di imprevisto al mio interno ti avviso”.

Questa precondizione in Java è implementata con una sintassi che mira a rendere disponibile sia il contesto del chiamante che i contenuti del chiamato. A questo punto viene bene pensarla come una specie di closure (si, continuo a citare le closures, Ruby fa malissimo). Aldilà del suono fighetto della parola il maggior vantaggio che penso si abbia dal citare le closures è spostare la locazione dell’esecuzione dentro al metodo chiamato, non il chiamante.

A questo punto il passo è breve, la mancanza di closures in Java mi obbliga ad una soluzione che forse in questo caso preferisco addirittura alla closure : il vincolo di gestione della checked exception come parametro del metodo.

Non per niente l’implementazione classica di una precondizione (debole) è la richiesta di un certo input, quindi un parametro.

Tutta questa storia per arrivare qui, ve l’avevo detto che era scialbetta.

Però mi piace molto, ho passato un bel po’ di tempo a ricamarci sopra, al punto che ho deciso che l’unica cosa per smettere era buttarla giù e desistere dal tentare inutilmente di dormire. Più ci ricamo più mi piace.

Come implemento la mia eccezione? Con un parametro che richiede un ExceptionManager, o se preferite un ExceptionListener. Il sistema delle eccezioni in Java non è una Chain of Responsibility dove la catena è lo stack di chiamata? Quindi tanto vale chiamarlo ExceptionListener.

Quindi, se l’immediato contesto del chiamante sa come gestire un’eccezione passerà al chiamato un ExceptionListener locale, che, non a caso, potrà portarsi dietro parte del contesto, sotto forma di variabili d’istanza del listener, per gestire il caso eccezionale.

public void chiamante(

chiamato(new ExceptionListener() {

public void manage() {

…gestisci eccezione…

}

});

)

Là dove normalmente propagherei l’eccezione con una throws propago invece il parametro e lo aggiungo alla dichiarazione del chiamante :

public void chiamante(ExceptionListener listener) {

….

chiamato(listener);

….

}

Notare la rimozione dell’if (try/catch) che di fatto era solo una ripetizione di un if dentro al chiamato che lanciava l’eccezione. Non ho mai capito perchè, per gestire un errore in un diverso contesto, dovessimo prima dire dentro al chiamato “se succede questo sgancia E” e poi dentro al chiamante “se ti arriva E fai questo”, così diciamo direttamente “se succede questo FAI quest’altro” punto, dove “quest’altro” è un bel listener che ci arriva da chissà dove. Un if è morto, lunga vita all’if.

Invece di propagare la dichiarazione dell’eccezione alla compilazione che deve per forza essere accoppiata da una specifica gestione, anch’essa stabilita staticamente, propago solo la dichiarazione del parametro alla compilazione, mentre posso iniettare al runtime la gestione dell’eccezione. Nel caso in cui la gestione sia molto lontana dal contesto del chiamante stiamo sfruttando l’iniezione delle dipendenze e quindi non stiamo vincolando il chiamante a una gestione particolare con dettagli che non gli competono, niente dipendenza. Invece di riempirmi di try/catch posso configurare i miei bean di Spring con dei bean di gestione delle eccezioni! L’avevo detto che ci avevo ricamato sopra no?

In breve è spiccicato quello che si ottiene con la sintassi classica, se non che questo promuove il riuso e evita gli switch che invece promuovono il copia-incolla.

Un altro ricamo :

E’ vero che nel listener ci portiamo dietro quello che ci serve del contesto del chiamante per gestire il caso eccezionale del chiamato, ma non abbiamo a disposizione il contesto del chiamato, ad esempio in questo caso :

public void transferFile(ExceptionListener pingFailureListener) {

if(pinga(host)) {

sendFile(host,file);

} else {

//se non è disponibile

pingFailureListener.manage();

};

}

Vorrei magari avere a disposizione “host” al momento della gestione.

Non rinnego infatti il ruolo delle eccezioni in quanto contenitori del contesto del chiamato, segrego solo le checked a quell’unico ruolo, lasciando alla definizione dei parametri il compito di imporre le precondizioni.

Da questo punto di vista eviterei ormai di chiamarle eccezioni e di dargli un nome più significativo del ruolo effettivo :

else {

//se non è disponibile

pingFailureListener.manage(new UnavailableHost(host,repetitionsSoFar));

}

O, quando il dato utile è uno solo, semplicemente passando il dato in sè, soprattutto se altrimenti creeremmo solo un data holder :

else {

//se non è disponibile

pingFailureListener.manage(host);

}

Qui i generics possono aiutarci a documentare meglio la precondizione e a evitare dei cast, ad esempio se host è un URL :

public void execute(ExceptionListener < URL > pingFailureListener) {…

o nell’esempio precedente :

public void execute(ExceptionListener < UnavailableHost > pingFailureListener) {…

Sempre per la serie dei ricami, notate che è UnavailableHost, non HostUnavailable, proprio perchè l’istanza che passiamo non ha più sapore di “evento” come un’eccezione invece ha, ma solo di fornitore di contesto, “unavailable” caratterizza solo l’identità di host, non descrive un accadimento.

Ancora un ricamo :

Ma ci siamo persi lo stack! Non potrò facilmente scrivere un listener che stampi lo stack delle chiamate!

In fin dei conti però, i casi in cui una checked exception ha come soluzione “printa lo stack e tanti saluti” sono proprio quelli in cui si dovrebbe parlare piuttosto di una unchecked exception.

Più scrivo più penso che siano una serie di banalità, ho rifattorizzato uno switch con uno strategy in fin dei conti. Ma, se si tiene in conto anche il caso del throws, che si risolve semplicemente con la propagazione del parametro, ci trovo un’eleganza che mi piace. Non a caso sono ancora sveglissimo alle 4.30 che penso a come sarebbe bello se eccezione diventasse sinonimo di unchecked exception mentre a fianco a Object, Throwable, nell’olimpo delle radici di della gerarchia ci fosse anche l’interfaccia ExceptionListener.

4.32, ultimo ricamo, giuro!

Lista dei parametri lunga!! Abiura!!

Vero. allunghiamo la lista dei parametri, ma è solo perchè riconosciamo che le checked erano semplicemente un’altra lista di precondizioni, ed è la somma delle due dovrebbe essere corta. Almeno così non possiamo infilare la testa nella sabbia : “Ho tre parametri e tre checked exception, tutto ok!” ora abbiamo sei parametri ed è chiaro che stiamo scrivendo schifezze. Inoltre così possiamo rifattorizzare via i parametri di troppo con le stesse regole che usavamo già per i parametri “tradizionali” (i listener possono diventare variabili d’istanza no?).

In breve, in questo modo si ha il comportamento di una checked exception, ma si forzano le parti in causa a gestirle in quanto tali : recuperabili, casi di funzionamento eccezionale ma nominale e riusabili.

4.40, Domani la pagherò molto cara.

Aggiunta del giorno dopo :
Non che mi attenda chissà quali commenti a questo post, ma per chi stesse leggendo questo su LiveDigital sappiate che, a meno di non essere registrati su LiveDigital, non potete commentare. Me ne rammarico, hanno 80mila funzioni ma non permettono i commenti sui blog. Classico. Non l’ho scelto io e non posso cambiare così presto. La buona notizia è che, a differenza di quanto mi era stato detto, quello su blogsource non è ancora sparito e quindi potete commentare su ratta.blogsource.com . Se volete a tutti i costi commentare su LiveDigital ma non volete iscrivervi mandatemi il commento per mail a carlo punto bottiglieri chiocciola gmail punto com e io provvederò a metterlo sul blog.
Che tristezza, vivo di copy paste proprio sul mio blog e ho un’interfaccia per i commenti che va contro i requisiti di qualunque utente.