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?