Strategia di progettazione per il wrapping delle eccezioni

4

Sto implementando un tipo di Repository per un framework / libreria che ha (approssimativamente) il seguente:

public interface FooRepository {

    boolean contains(String id);

    Foo fetch(String id);

    void commit(Foo foo):
}

Possiamo implementarlo in molti modi diversi, a seconda del supporto di archiviazione che stiamo usando; per esempio

public class FileFooRepositiory implements FooRepository {

    public boolean contains(String id) {
        return fileExistsInFileSystem(id);
    }

    public Foo fetch(final String id) {
        return parseFooFromFile(id);
    }

    public void commit(final Foo foo) {
        writeFooToFile(foo);
    }
}

Ma possiamo anche avere

public class SQLFooRepositiory implements FooRepository {

    public SQLFooRepository(Connection connection) {
        // ...
    }

    public boolean contains(String id) {
        return fooExistsInDatabase(id);
    }

    public Foo fetch(final String id) {
        return parseFooFromDatabase(id);
    }

    public void commit(final Foo foo) {
        writeFooToDatabase(foo);
    }
}

Possono esserci anche implementazioni che memorizzano Foo oggetti in Map<String, Foo> o quelli che usano sistemi di archiviazione esterni (appfabric, redis, ecc.)

Tuttavia, ci sono cose che possono impedire a ciascuna di queste implementazioni di fare ciò che dovrebbero fare. Ognuno di loro lancia un diverso tipo di Exception . Per SQLException , la maggior parte delle cose che potrebbero accadere userebbe questa unica eccezione. Per i file, sarebbe una sorta di IOException , in particolare, FileNotFoundException , AccessDeniedException , ecc.

La mia domanda è questa: come posso modificare il contratto dell'interfaccia FooRepository per consentire il lancio di questi tipi di Exception s, senza utilizzare la clausola throws Exception . Il mio pensiero iniziale era di avere un RepositoryException che sarebbe extend RuntimeException e avvolgere le eccezioni reali

try {
    // ...
}
catch (SQLException sqle) {
    throw new RepositoryException(sqle);
}

ma non sono sicuro che questa sia la strategia corretta che dovrei usare.

    
posta Zymus 28.07.2015 - 07:54
fonte

4 risposte

4

La tua soluzione è corretta: il repository dovrebbe lanciare un tipo RepositoryException generico per segnalare che qualcosa è andato storto. Si avvolge l'eccezione effettiva in RepositoryException in modo che la registrazione possa accedere ai dati di cui ha bisogno e che i consumatori possano intercettare l'eccezione generica e gestirla da qualche parte lungo la linea. L'avvolgimento delle eccezioni reali nasconde i dettagli dell'implementazione del repository concreto dal consumatore.

L'unica ragione per distinguere tra le eccezioni (ad esempio, restituire eccezioni specifiche dall'interfaccia) è quando la gestione di un tipo di eccezione dovrebbe essere diversa dall'altra. In questo caso non riesco davvero a pensare a un motivo per cui gestiresti le eccezioni dal repository in modo diverso, quindi usa semplicemente RepositoryException.

    
risposta data 28.07.2015 - 09:18
fonte
1

Penso che la tua strategia sia corretta (e accetto che le opinioni possano variare rispetto a "il migliore").

Al client non dovrebbe interessare se il repository sottostante è un file o un server DB, a loro importa che il commit sia fallito - devono prima saperlo e gestirlo. Dopodiché, se lo desiderano (ad esempio richiedono un intervento dell'utente o una registrazione dettagliata), prova ad affrontare il problema sottostante. Lanciando un'eccezione che rappresenta quell'errore, RepositoryException è la cosa giusta da fare. Se l'unica eccezione è sufficiente, usa solo quella. Se gli errori sono più complessi, includi più eccezioni, ma sii giudizioso.

In genere consiglio che le eccezioni generate rappresentano la condizione di errore che si desidera inoltrare al client - un "errore semantico" se si vuole, non solo la causa principale. Le eccezioni interne (java getCause ) sono buone per esprimere la causa principale dell'errore che si è verificato.

Fondamentalmente, sostengo che l'eccezione è "Non potrei farlo, perché il repository non è disponibile", l'eccezione interna è "Non sono riuscito a trovare il file perché l'utente non ha il permesso."

La definizione delle eccezioni comporta una linea sottile tra l'offerta della ricchezza di dettagli richiesta per descrivere l'errore per aiutare il codice client a correggerlo e non essendo troppo prolisso che diventa un peso da catturare e gestire.

    
risposta data 28.07.2015 - 08:37
fonte
0

Il tuo approccio è corretto e se hai bisogno che il tuo repository client possa distinguere tra le eccezioni del repository, forse per mostrare diversi tipi di errori, potresti anche creare un albero di Eccezioni del repository (ereditando sempre RuntimeException) come Spring .

public class IntegrityConstraintViolationextends RepositoryException {

}

public class Client1 {
  public void consumeRepository(){
    try {
      getRepository.fetch
    } 
    catch (IntegrityConstraintViolatione1){
      // do Something special if there is a problem with integrity violation
    }
    catch (RepositoryException e2){
      // do something in any other case
    }
  }
}

In questo caso, è necessario che l'implementazione del repository sottostante abbia la capacità di informare le violazioni del vincolo di integrità. Ad esempio, il driver JDBC lancia IntegrityConstraintViolation

try {
    // ...
}
catch (SQLIntegrityConstraintViolationException sqle) {
    throw new IntegrityConstraintViolation(sqle);
}

Il vantaggio è che lavori sempre con il tuo albero gerarchico di eccezioni.

    
risposta data 28.07.2015 - 13:56
fonte
0

Ci sono stato.

Quello che ho fatto è stato il seguente:

Nella classe del repository di file che implementa FooRepository :

    try{            
       // try to store
    } catch (IOException e) {
        throw new CouldntStoreFooException(e.getLocalizedMessage());
    } 

    try{            
       // try to retrieve
    } catch (IOException e) {
        throw new CouldntRetrieveFooException(e.getLocalizedMessage());
    } 

Nella classe di repository del database che implementa FooRepository :

    try{            
       // try to store
    } catch (SQLException e) {
        throw new CouldntStoreFooException(e.getLocalizedMessage());
    } etc

    try{            
       // try to retrieve
    } catch (SQLException e) {
        throw new CouldntRetrieveFooException(e.getLocalizedMessage());
    }

In questo modo la firma dell'interfaccia conosce solo CouldntRetrieveFooException o CouldntRetrieveFooException ma quando ottieni l'eccezione e vedi il log, puoi vedere il problema reale perché hai passato il problema originale ( e.getLocalizedMessage() ) come messaggio nel costruttore.

Le eccezioni non hanno bisogno di racchiudere nulla, solo:

public class CouldntStoreFooException extends Exception {

    public CouldntStoreFooException(String reason) {
        super("Problem retrieving foo "+reason);
    }

}

...

public class CouldntRetrieveFooException extends Exception {

    public CouldntRetrieveFooException(String reason) {
        super("Problem retrieving foo "+reason);
    }

}
    
risposta data 28.07.2015 - 20:59
fonte

Leggi altre domande sui tag