venerdì 2 gennaio 2015

La gestione degli errori nella programmazione Agile

Nella programmazione Agile una delle scelte chiavi è come gestire gli errori tirati (thrown) dalle classi custom che compongono la propria applicazione.

Tranquilli, non voglio riprendere in mano la vecchia polemica tra eccezioni checked e non-checked. Da diversi anni (da Spring in poi, direi) i framework  tendono tutti a proporre eccezioni non-checked, ma ci sono in giro diversi programmatori convinti che in molti casi le eccezioni checked abbiano dei vantaggi.

Il Web straripa di commenti al riguardo. Giusto per farsi un'idea, per chi fosse nuovo a questa polemica, questo link è un buon punto di inizio.

Supponiamo di avere in corso lo sviluppo di una libreria (chiamiamola MyLibrary) che esegue delle operazioni che possono andare male, e che si ritenga che il modo migliore di comunicare con il layer superiore, quello che normalmente chiama i metodi della libreria, sia di tirare delle eccezioni unchecked. il mio suggerimento, derivato al 90% dai 6 tips di Dale Taylor, è il seguente:

Create una interfaccia, chiamatela MyLibraryErrorCode e fatela esporre il metodo getKey()

public interface MyLibraryErrorCode{
    public String getKey()
} 

Create una RuntimeException e nominatela in modo che sia evidente il fatto che tramite questa RuntimeException gestirete le eccezioni dell'intera vostra Library. 

Ovviamente, potete subclassare Exception se volete gestire eccezioni checked.  Oppure avere una famiglia di eccezioni checked ed una unchecked e gestirle entrambe con lo stesso approccio. Come già spiegato, lo scopo di questo post non è di contribuire alla diatriba tra checked e unchecked, ma solo proporre un approccio alla loro gestione.

public class MyLybraryException extends RuntimeException{

    private MyLibraryErrorCode errorCode;
    private Map<String,Object> attributes=new HashMap<String,Object>();


    public MyLibraryException(String message, MyLibraryErrorCode errorCode){
        super(message);
        this.errorCode=errorCode;
    }

    public MyLibraryException(MyLibraryErrorCode errorCode){
        this.errorCode=errorCode;
    }

    public MyLibraryErrorCode getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(MyLibraryErrorCode errorCode) {
        this.errorCode = errorCode;
    }
    public MyLibraryException set(String key,Object value){
        attributes.put(key,value);
        return (this);
    }

    public Object get(String key){
        return attributes.get(key);
    }
}

Tramite questa classe potrete creare RuntimeExceptions di tipo MyLibraryException dotate di un codice di errore di tipo MyLibraryErrorCode e di un array di key-value in grado di registrare una lista di attributi arbitrari che saranno utili per poter meglio documentare l'Exception nei layer superiori dell'applicazione.

Create degli Enum che implementino l'interfaccia MyLibraryErrorCode

Il codice sarà, ad esempio, il seguente:
public enum BasicsError implements MyLibraryErrorCode{
    NO_NUMERATOR,
    BAD_KEY,
    BAD_FORMAT  

public String getKey(){
        return this.getClass().getSimpleName()+"_"+this.toString();
    }
}

Nell'esempio della MyLibrary, quindi, tramite questo Enum gli errori classificati come BasicError verranno identificati da uno specifico codice (NO_NUMERATOR, BAD_KEY oppure BAD_FORMAT). La classe BasicError mette inoltre a disposizione un metodo (getKey) che sarà utile ad una rappresentazione compatta del codice di errore da utilizzare nella propria gestione multlingua degli errori.

Vediamo adesso un esempio di utilizzo di questo metodo.

Esempio di utilizzo  

Supponiamo di avere un punto nel codice della MyLibrary dove è necessario tirare delle eccezioni permettendo allo strato superiore del codice di costruire, a fronte delle stesse, una messagistica multilanguage chiara e dettagliata.

Un esempio potrebbe essere questo snippet:
 
    NumeratorFeeder nf = getNumeratorFeeder(code, date);
    if (nf == null)
        throw new MyLibraryException(BasicsError.NO_NUMERATOR)
        .set("code", code)
        .set("date", date);
    else {
       .......

In questo esempio si può osservare come:
  1. sia tirata una eccezione di tipo MyLibraryException;
  2. l'eccezione sia stata tipizzata cone NO_NUMERATOR
  3. il vettore attributes dell'eccezione sia stato alimentato con le key-value relative ai valori dei campi code e di date presenti al momento in cui è stata tirata l'eccezione.
 Il codice dello strato superiore potrebbe essere così organizzato:

try {
   assertEquals(234, numeratorRep.getNextNum("invNum", new DateTime(2999, 4, 12, 0, 0)));
} catch (MyLibraryException e) {
   if(e.getErrorCode().equals(BasicsError.NO_NUMERATOR)) {
      String italianMessage = context.getMessage(e.getErrorCode().getKey(), new Object[]{"invNum", new DateTime(2999, 4, 12, 0, 0).toString(DateTimeFormat.shortDate())}, Locale.ITALIAN);
      String englishMesage = context.getMessage(e.getErrorCode().getKey(), new Object[]{"invNum", new DateTime(2999, 4, 12, 0, 0).toString(DateTimeFormat.shortDate())}, Locale.ENGLISH);
      String itmsg = String.format("Nessun numeratore trovato per numeratore con codice %s alla data %s", e.get("code"), ((DateTime) e.get("date")).toString(DateTimeFormat.shortDate()));
      assertEquals(italianMessage, itmsg);
      String enmsg = String.format("No numerator found for code %s and date %s", e.get("code"), ((DateTime) e.get("date")).toString(DateTimeFormat.shortDate()));
      assertEquals(englishMesage, enmsg);
      }
}

Si noti quanto segue:
  1. l'eccezione tirata non è specifica per il caso NO_NUMERATOR, ma generica per tutta la MyLibrary. In questo modo si può evitare di dover definire decine di eccezioni custom con nomi fantasiosi. Basta un tipo di eccezione (due se si desidera avere eccezioni non-checked insieme a eccezioni checked);
  2. il motivo per cui è stata tirata l'eccezione è contenuto nell' ErrorCode
  3. insieme all'errorCode è possibile accedere all'array di key-value dove sono descritte le circostanze per cui l'errore è avvenuto. Ciò è particolarmente utile in elaborazioni batch dove l'errore può essere dovuto a dati malformati, key inesistenti, ecc.
  4. il messaggio di errore può essere così composto  in modo multi-language unendo un testo costante con i valori contenuti nel messaggio di errore.
Con questo approccio è stata costruita la libreria sottostante a Tyl Framework, basato su Vaadin e MongoDb,  che è orientato a fornire una piattaforma per lo sviluppo di applicazioni gestionali con metodi agili.

Nessun commento: