Semplice ma frequente lancio vs ragionevole ma raramente lancio vs complesso ma mai lancio [duplicato]

0

Un sacco di codice progettato per convertire o analizzare alcuni dati di tipo Foo in Bar viene scritto supponendo che non si possano passare intenzionalmente input non validi. In quanto tale, presuppone che tutto sia corretto e genera eccezioni se in seguito si rende conto che qualcosa non va.

Questa è tipicamente una cosa appropriata da fare; aspettatevi che gli input siano validi, buttate se non lo sono. Sfortunatamente, ci sono alcuni casi in cui il chiamante ha un dato che potrebbe rappresentare diversi tipi. Il modo più semplice per farlo sarebbe quello di provare ciascuno dei possibili parser e utilizzare l'output di qualsiasi parser non si lamenta di ciò.

Come esempio un po 'forzato ma adatto, diciamo che stai analizzando i dati JSON in arrivo. Secondo alcune specifiche, una determinata proprietà stringa può essere sia un numero (come stringa), una delle poche parole chiave magiche definite in un enum, o qualche altro testo in chiaro, ognuno dei quali deve chiamare un corrispondente add(int) ,% co_de metodo% o add(MyEnum) .

Il modo più semplice si riduce sostanzialmente a

try {
    add(Integer.parseInt(input));
} catch (SomeException e) {
    try {
        add(MyEnum.valueOf(input));
    } catch (IllegalArgumentException e) {
        add(input);
    }
}

Questo palesemente va contro il mantra spesso ripetuto di "Non controllare il flusso usando le eccezioni", cade preda del temuto costo delle prestazioni dell'uso di add(String) e try , e va anche contro l'idea che le eccezioni sono solo per, beh, circostanze eccezionali che in genere non dovrebbero accadere; qui, fondamentalmente stiamo dicendo esplicitamente "sì, ci sarà probabilmente un'eccezione o due qui prima che abbiamo finito".

Usando throw come esempio principale, un approccio leggermente più carino sarebbe quello di creare un metodo di utilità lungo le linee di

public static OptionalInt tryParseInt(String s) {
    try {
        return OptionalInt.of(Integer.parseInt(s));
    } catch (NumberFormatException e) {
        return OptionalInt.empty();
    }
}

In pratica si tratta solo di nascondere esattamente gli stessi problemi dietro una bella facciata. Certo, rende il codice chiamante più pulito e incapsula la bruttezza in un metodo dedicato a contenerlo, ma non risolve i problemi sottostanti. Tuttavia, si adatta bene alla regola "Keep it simple, stupid".

Una terza opzione dovrebbe semplicemente implementare da zero il mio parseInt . Sfortunatamente, ciò significa che devo duplicare funzionalità ben consolidate e probabilmente ben ottimizzate nel JDK. Nel caso più generale, se sto usando una libreria per analizzare il mio tryParseInt in Foo , è probabile perché l'analisi è complicata da cominciare. Quindi, questo è un caso di reinventare la ruota invece di riutilizzare la funzionalità esistente, che quasi sicuramente la rende una soluzione considerevolmente peggiore in tutti i casi meno banali.

L'opzione 4A è scrivere un metodo Bar che prima convalida l'input, restituendo tryParseInt se la convalida fallisce e passa l'input a OptionalInt.empty() se la convalida ha esito positivo. Questo ha due inconvenienti però. Innanzitutto, mentre non reimplementa la funzionalità completa di Integer.parseInt , esegue comunque la stessa validazione eseguita implicitamente dal metodo JDK. Stiamo evitando del tutto le eccezioni, ma al costo di dover sostanzialmente replicare la logica ben consolidata. Il secondo problema è che non è sempre facile scrivere validazioni completamente corrette senza scrivere in modo efficace un parser, quindi ...

L'opzione 4B è di scrivere una logica di convalida ottimistica che funzioni in tutti i casi comuni e probabili, ma può comunque far passare alcuni casi speciali. La chiamata al parser sottostante verrebbe quindi avvolta in un try-catch con l'ipotesi che il catch verrà usato raramente. Sempre usando Integer.parseInt come esempio, potremmo verificare se la stringa corrisponde alla regex tryParseInt per coprire il 99% dei casi. [+\-]?\d{1,10} e [2147483648, 9999999999] verrebbero comunque lasciati passare, lanciati e catturati, ma è improbabile che si verifichino. Anche questo non risolve i problemi strutturali sottostanti, ma previene come il 99% dell'impatto sulle prestazioni di dover generare un'eccezione (supponendo che la convalida sia veloce).

Ancora, [-9999999999, -2147483649] e il resto sono solo semplici esempi di un problema più generale quando si tratta di parser di terze parti e simili.

Fondamentalmente, la mia domanda si riduce a come scendere a compromessi tra le diverse regole empiriche, intrinsecamente incompatibili. Vogliamo abolire ciecamente le eccezioni intenzionali del tutto? Vogliamo limitare la maggior parte dell'impatto sulle prestazioni delle eccezioni senza aggiungere troppa complessità? La semplicità e il massimo riutilizzo del codice superano questi "dettagli nitidi" e "micro-ottimizzazioni premature"?

Esiste una best practice comunemente accettata per situazioni come questa?

    
posta Smallhacker 02.08.2018 - 14:43
fonte

1 risposta

3

Il motivo per cui stai lottando con questo è che non esiste una risposta chiara. La situazione è ambigua.

È utile ricordare che le regole pratiche sono linee guida, non requisiti assoluti.

La gestione delle eccezioni funziona al meglio quando viene rilevata in profondità nel codice e gestita in remoto (in alcuni casi di ripristino di alto livello). Ma è uno strumento che puoi usare come preferisci. La regola più importante è il pragmatismo.

Per quanto riguarda le cose come analizzare gli ints, o json - questi sono chiaramente ambigui. Probabilmente il più delle volte, i fallimenti in questi tipi di strumenti si tradurranno in eccezioni - e probabilmente il più delle volte, questo è il migliore. Ma se hai un'applicazione in cui non è questo il caso, NON c'è NIENTE sbagliato nel trasformare queste utilità di lancio delle eccezioni in utility di ritorno del codice di errore (tramite il wrapping che hai descritto).

In generale, un'altra linea guida che ti consiglierei di prendere in considerazione - per andare avanti con i due con cui stai già lottando - è "quando a Roma, fai come i romani" o "il principio della sorpresa minima". Se stai usando java, e parseInt, preferisci scrivere il tuo codice per lavorare con il lancio. Se stai usando C ++, dove sscanf / strtol etc non getti, controlla i codici di errore.

Un'altra buona regola empirica è "come puoi rendere il tuo codice più conciso?" La brevità non è sempre utile, ma se si guarda il codice e c'è un sacco di boilterplate a causa di fare le cose in un modo (eccezioni o codici di errore), provare il contrario e vedere se il suo più semplice o più chiara.

La buona notizia è che l'unico fattore che USED è (in qualche modo) pertinente, ma che ora è molto meno rilevante, è quello delle prestazioni. È molto improbabile in qualsiasi applicazione moderna, che la differenza di prestazioni tra questi approcci sia importante.

    
risposta data 02.08.2018 - 16:17
fonte

Leggi altre domande sui tag