alternative ai tentativi di cattura nidificati per fallback


14

Ho una situazione in cui sto provando a recuperare un oggetto. Se la ricerca non riesce, sono presenti diversi fallback, ciascuno dei quali potrebbe non riuscire. Quindi il codice è simile al seguente:

try {
    return repository.getElement(x);
} catch (NotFoundException e) {
    try {
        return repository.getSimilarElement(x);
    } catch (NotFoundException e1) {
        try {
            return repository.getParentElement(x);
        } catch (NotFoundException e2) {
            //can't recover
            throw new IllegalArgumentException(e);
        }
    }
}

Sembra terribilmente brutto. Odio restituire null, ma è meglio in questa situazione?

Element e = return repository.getElement(x);
if (e == null) {
    e = repository.getSimilarElement(x);
}
if (e == null) {
    e = repository.getParentElement(x);
}
if (e == null) {
    throw new IllegalArgumentException();
}
return e;

Ci sono altre alternative?

L'utilizzo di blocchi nidificati try-catch è un anti-pattern? è correlato, ma le risposte ci sono sulla falsariga di "a volte, ma di solito è evitabile", senza dire quando o come evitarlo.


1
È NotFoundExceptiondavvero eccezionale?

Non lo so, ed è probabilmente per questo che ho problemi. Questo è in un contesto di e-commerce, in cui i prodotti vengono interrotti quotidianamente. Se qualcuno aggiunge un segnalibro a un prodotto che viene successivamente interrotto e quindi tenta di aprire il segnalibro ... è eccezionale?
Alex Wittig,

@FiveNine secondo me, sicuramente no - è prevedibile. Vedi stackoverflow.com/questions/729379/…
Konrad Morawski

Risposte:


17

Il modo normale per eliminare l'annidamento è utilizzare le funzioni:

Element getElement(x) {
    try {
        return repository.getElement(x);
    } catch (NotFoundException e) {
        return fallbackToSimilar(x);
    }  
}

Element fallbackToSimilar(x) {
    try {
        return repository.getSimilarElement(x);
     } catch (NotFoundException e1) {
        return fallbackToParent(x);
     }
}

Element fallbackToParent(x) {
    try {
        return repository.getParentElement(x);
    } catch (NotFoundException e2) {
        throw new IllegalArgumentException(e);
    }
}

Se queste regole di fallback sono universali, potresti prendere in considerazione l'implementazione direttamente repositorynell'oggetto, dove potresti essere in grado di usare solo semplici ifistruzioni anziché un'eccezione.


1
In questo contesto, methodsarebbe una parola migliore di function.
Sulthan,

12

Questo sarebbe davvero facile con qualcosa come una monade Opzione. Sfortunatamente, Java non ne ha. In Scala, userei il Trytipo per trovare la prima soluzione di successo.

Nella mia mentalità di programmazione funzionale, avevo creato un elenco di callback che rappresentavano le varie fonti possibili e li scorrevo fino a trovare il primo successo:

interface ElementSource {
    public Element get();
}

...

final repository = ...;

// this could be simplified a lot using Java 8's lambdas
List<ElementSource> sources = Arrays.asList(
    new ElementSource() {
        @Override
        public Element get() { return repository.getElement(); }
    },
    new ElementSource() {
        @Override
        public Element get() { return repository.getSimilarElement(); }
    },
    new ElementSource() {
        @Override
        public Element get() { return repository.getParentElement(); }
    }
);

Throwable exception = new NoSuchElementException("no sources set up");
for (ElementSource source : sources) {
    try {
        return source.get();
    } catch (NotFoundException e) {
        exception = e;
    }
}
// we end up here if we didn't already return
// so throw the last exception
throw exception;

Questo può essere consigliato solo se si dispone davvero di un numero elevato di fonti o se è necessario configurare le fonti in fase di esecuzione. Altrimenti, questa è un'astrazione inutile e trarrai maggiori profitti dal mantenere il tuo codice semplice e stupido e usare solo quei brutti tentativi annidati.


+1 per menzionare il Trytipo in Scala, per menzionare monadi e per la soluzione usando un ciclo.
Giorgio,

Se fossi già su Java 8, ci proverei, ma come dici tu, è un po 'troppo per pochi fallback.
Alex Wittig,

1
In realtà, al momento della pubblicazione di questa risposta, Java 8 con supporto per la Optionalmonade ( prova ) era già stato rilasciato.
MKalkov,

3

Se si prevede che molte di queste chiamate al repository verranno lanciate NotFoundException, è possibile utilizzare un wrapper attorno al repository per semplificare il codice. Non lo consiglierei per le normali operazioni, intendiamoci:

public class TolerantRepository implements SomeKindOfRepositoryInterfaceHopefully {

    private Repository repo;

    public TolerantRepository( Repository r ) {
        this.repo = r;
    }

    public SomeType getElement( SomeType x ) {
        try {
            return this.repo.getElement(x);
        }
        catch (NotFoundException e) {
            /* For example */
            return null;
        }
    }

    // and the same for other methods...

}

3

Su suggerimento di @ amon, ecco una risposta più monadica. È una versione molto ridotta, in cui devi accettare alcune ipotesi:

  • la funzione "unit" o "return" è il costruttore della classe

  • l'operazione "bind" avviene al momento della compilazione, quindi è nascosta dall'invocazione

  • le funzioni "azione" sono anche legate alla classe al momento della compilazione

  • sebbene la classe sia generica e racchiuda qualsiasi classe arbitraria E, penso che in questo caso sia davvero eccessivo. Ma l'ho lasciato in questo modo come esempio di cosa potresti fare.

Con queste considerazioni, la monade si traduce in una classe wrapper fluente (anche se stai rinunciando a molta flessibilità che avresti in un linguaggio puramente funzionale):

public class RepositoryLookup<E> {
    private String source;
    private E answer;
    private Exception exception;

    public RepositoryLookup<E>(String source) {
        this.source = source;
    }

    public RepositoryLookup<E> fetchElement() {
        if (answer != null) return this;
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookup(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public RepositoryLookup<E> orFetchSimilarElement() {
        if (answer != null) return this; 
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookupVariation(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public RepositoryLookup<E> orFetchParentElement() {
        if (answer != null) return this; 
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookupParent(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public boolean failed() {
        return exception != null;
    }

    public Exception getException() {
        return exception;
    }

    public E getAnswer() {
        // better to check failed() explicitly ;)
        if (this.exception != null) {
            throw new IllegalArgumentException(exception);
        }
        // TODO: add a null check here?
        return answer;
    }
}

(questo non verrà compilato ... alcuni dettagli vengono lasciati incompiuti per mantenere il campione piccolo)

E l'invocazione sarebbe simile a questa:

Repository<String> repository = new Repository<String>(x);
repository.fetchElement().orFetchParentElement().orFetchSimilarElement();

if (repository.failed()) {
    throw new IllegalArgumentException(repository.getException());
}

System.err.println("Got " + repository.getAnswer());

Nota che hai la flessibilità di comporre le operazioni di "recupero" come preferisci. Si interromperà quando riceverà una risposta o un'eccezione diversa da quella non trovata.

L'ho fatto molto velocemente; non è del tutto giusto, ma si spera trasmetta l'idea


1
repository.fetchElement().fetchParentElement().fetchSimilarElement();- a mio avviso: codice malvagio (nel senso dato da Jon Skeet)
Konrad Morawski

ad alcune persone questo stile non piace, ma l'utilizzo return thisper creare concatenazioni di chiamate a oggetti è in circolazione da molto tempo. Poiché OO coinvolge oggetti mutabili, return thisè più o meno equivalente a return nullsenza incatenamento. Tuttavia, return new Thing<E>apre le porte a un'altra funzionalità in cui questo esempio non rientra, quindi è importante per questo modello se si sceglie di seguire questa strada.
Rob,

1
Ma mi piace quello stile e non sono contrario a concatenare chiamate o interfacce fluide in quanto tali. C'è comunque una differenza tra CustomerBuilder.withName("Steve").withID(403)e questo codice, perché solo dal vedere .fetchElement().fetchParentElement().fetchSimilarElement()non è chiaro cosa accada, e questa è la cosa chiave qui. Vengono tutti recuperati? In questo caso non è cumulativo e quindi non così intuitivo. Devo vederlo if (answer != null) return thisprima di capirlo davvero. Forse è solo una questione di nomi propri ( orFetchParent), ma è comunque "magico".
Konrad Morawski,

1
A proposito (so che il tuo codice è troppo semplificato e solo una dimostrazione di concetto), sarebbe bene forse restituire un clone di answerin getAnswere ripristinare (cancellare) il answercampo stesso prima di restituire il suo valore. Altrimenti in qualche modo infrange il principio di separazione comando / query, perché chiedere di recuperare un elemento (interrogare) altera lo stato dell'oggetto repository ( answernon viene mai ripristinato) e influisce sul comportamento di fetchElementquando lo chiami la prossima volta. Sì, sto facendo un pignolo, penso che la risposta sia valida, non sono stato io a declassarlo.
Konrad Morawski,

1
è un buon punto. Un altro modo sarebbe "tryToFetch ...". Il punto importante è che in questo caso vengono chiamati tutti e 3 i metodi, ma in un altro caso un client potrebbe semplicemente utilizzare "tryFetch (). TryFetchParent ()". Inoltre, chiamarlo "repository" è sbagliato, perché sta davvero modellando un singolo recupero. Forse mi occuperò dei nomi per chiamarlo "RepositoryLookup" e "tentare" di chiarire che si tratta di un artefatto temporaneo a colpo singolo che fornisce una certa semantica attorno a una ricerca.
Rob,

2

Un altro modo per strutturare una serie di condizioni come questa è portare una bandiera, oppure testare null (ancora meglio, usare Guava's Optional per determinare quando è presente una buona risposta) al fine di concatenare le condizioni.

Element e = null;

try {
    e = repository.getElement(x);
} catch (NotFoundException e) {
    // nope -- try again!
}

if (e == null) {  // or ! optionalElement.isPresent()
    try {
        return repository.getSimilarElement(x);
    } catch (NotFoundException e1) {
        // nope -- try again!
    }
}

if (e == null) {  // or ! optionalElement.isPresent()
    try {
        return repository.getParentElement(x);
    } catch (NotFoundException e2) {
        // nope -- try again!
    }
}

if (e == null) {  // or ! optionalElement.isPresent()
    //can't recover
    throw new IllegalArgumentException(e);
}

return e;

In questo modo, stai osservando lo stato dell'elemento e stai effettuando le chiamate giuste in base al suo stato, vale a dire fino a quando non hai ancora una risposta.

(Sono d'accordo con @amon, però. Consiglio di guardare un modello Monad, con un oggetto wrapper come class Repository<E>quello che ha membri E answer;e Exception error;. Ad ogni fase controlla se c'è un'eccezione e, in tal caso, salta ogni passaggio rimanente. Alla fine, ti rimane una risposta, l'assenza di una risposta o un'eccezione e puoi decidere cosa farne.)


-2

In primo luogo, mi sembra che ci dovrebbe essere una funzione come repository.getMostSimilar(x)(dovresti scegliere un nome più appropriato) in quanto sembra esserci una logica che viene utilizzata per trovare l'elemento più vicino o più simile per un dato elemento.

Il repository può quindi implementare la logica come mostrato nel messaggio di Amons. Ciò significa che l'unico caso in cui deve essere generata un'eccezione è quando non è possibile trovare un singolo elemento.

Tuttavia, ciò è ovviamente possibile solo se le logiche per trovare l'elemento più vicino possono essere incapsulate nel repository. Se ciò non è possibile, fornire ulteriori informazioni su come (in base a quali criteri) è possibile scegliere l'elemento più vicino.


le risposte sono per rispondere alla domanda, non per chiedere chiarimenti
moscerino

Bene, la mia risposta sta risolvendo il suo problema in quanto mostra un modo per evitare il tentativo / cattura annidato in determinate condizioni. Solo se queste condizioni non sono soddisfatte abbiamo bisogno di maggiori informazioni. Perché questa non è una risposta valida?
valenterry,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.