Lanciare un'eccezione è un anti-modello qui?


33

Ho appena avuto una discussione su una scelta di design dopo una revisione del codice. Mi chiedo quali siano le tue opinioni.

C'è questa Preferencesclasse, che è un secchio per le coppie chiave-valore. I valori nulli sono legali (è importante). Prevediamo che alcuni valori potrebbero non essere ancora stati salvati e vogliamo gestirli automaticamente inizializzandoli con un valore predefinito predefinito quando richiesto.

La soluzione discussa utilizzava il seguente modello (NOTA: questo non è il codice reale, ovviamente - è semplificato a scopi illustrativi):

public class Preferences {
    // null values are legal
    private Map<String, String> valuesFromDatabase;

    private static Map<String, String> defaultValues;

    class KeyNotFoundException extends Exception {
    }

    public String getByKey(String key) {
        try {
            return getValueByKey(key);
        } catch (KeyNotFoundException e) {
            String defaultValue = defaultValues.get(key);
            valuesFromDatabase.put(key, defaultvalue);
            return defaultValue;
        }
    }

    private String getValueByKey(String key) throws KeyNotFoundException {
        if (valuesFromDatabase.containsKey(key)) {
            return valuesFromDatabase.get(key);
        } else {
            throw new KeyNotFoundException();
        }
    }
}

È stato criticato come anti-modello - abusando delle eccezioni per controllare il flusso . KeyNotFoundException- portato alla vita solo per quel caso d'uso - non sarà mai visto fuori dallo scopo di questa classe.

Sono essenzialmente due metodi che giocano a prendere con esso semplicemente per comunicare qualcosa tra loro.

La chiave non presente nel database non è qualcosa di allarmante o eccezionale: ci aspettiamo che ciò avvenga ogni volta che viene aggiunta una nuova impostazione delle preferenze, quindi il meccanismo che la inizializza con garbo con un valore predefinito, se necessario.

La controargomentazione era che getValueByKey- il metodo privato - come definito in questo momento non ha modo naturale di informare il metodo pubblico sia sul valore, sia sulla chiave presente. (In caso contrario, deve essere aggiunto in modo che il valore possa essere aggiornato).

Il ritorno nullsarebbe ambiguo, dal momento che nullè un valore perfettamente legale, quindi non si può dire se significasse che la chiave non c'era, o se c'era un null.

getValueByKeydovrebbe restituire una sorta di a Tuple<Boolean, String>, il bool impostato su true se la chiave è già lì, in modo che possiamo distinguere tra (true, null)e (false, null). (Un outparametro potrebbe essere usato in C #, ma è Java).

È un'alternativa più bella? Sì, dovresti definire una classe monouso per l'effetto di Tuple<Boolean, String>, ma poi ci stiamo liberando KeyNotFoundException, quindi quel tipo di equilibri. Stiamo anche evitando il sovraccarico di gestire un'eccezione, anche se non è significativo in termini pratici - non ci sono considerazioni sulle prestazioni di cui parlare, è un'app client e non è come le preferenze dell'utente verranno recuperate milioni di volte al secondo.

Una variante di questo approccio potrebbe usare Guava Optional<String>(Guava è già usato in tutto il progetto) invece di qualche abitudine Tuple<Boolean, String>, e quindi possiamo distinguere tra Optional.<String>absent()"corretto" null. Sembra comunque hacker per ragioni che sono facili da vedere - l'introduzione di due livelli di "nullità" sembra abusare del concetto che stava dietro la creazione di Optionals in primo luogo.

Un'altra opzione sarebbe quella di verificare esplicitamente se la chiave esiste (aggiungere un boolean containsKey(String key)metodo e chiamare getValueByKeysolo se abbiamo già affermato che esiste).

Infine, si potrebbe anche incorporare il metodo privato, ma l'attuale getByKeyè in qualche modo più complesso del mio esempio di codice, quindi l'allineamento lo farebbe sembrare piuttosto brutto.

Potrei spaccare i capelli qui, ma sono curioso di sapere su cosa scommetteresti per essere il più vicino alle migliori pratiche in questo caso. Non ho trovato una risposta nelle guide di stile di Oracle o Google.

L'uso di eccezioni come nel codice di esempio è un anti-pattern o è accettabile dato che le alternative non sono molto pulite? Se lo è, in quali circostanze andrebbe bene? E viceversa?


1
@ GlenH7 la risposta accettata (e più votata) riassume che "dovresti usarli [eccezioni] nei casi in cui rendono più semplice la gestione degli errori con meno ingombro di codice". Immagino che sto chiedendo qui se le alternative che ho elencato debbano essere considerate "meno disordine di codice".
Konrad Morawski,

1
Ho ritirato il mio VTC: mi stai chiedendo qualcosa di correlato, ma diverso dal duplicato che ho suggerito.

3
Penso che la domanda diventi più interessante quando getValueByKeyè anche pubblica.
Doc Brown,

2
Per riferimento futuro, hai fatto la cosa giusta. Questo sarebbe stato fuori tema sulla revisione del codice perché è un codice di esempio.
RubberDuck,

1
Giusto per entrare --- questo è perfettamente idiomatico Python, e sarebbe (penso) il modo preferito per gestire questo problema in quella lingua. Ovviamente, però, lingue diverse hanno convenzioni diverse.
Patrick Collins,

Risposte:


75

Sì, il tuo collega ha ragione: è un brutto codice. Se un errore può essere gestito localmente, deve essere gestito immediatamente. Un'eccezione non deve essere generata e gestita immediatamente.

Questo è molto più pulito della tua versione (il getValueByKey()metodo è stato rimosso):

public String getByKey(String key) {
    if (valuesFromDatabase.containsKey(key)) {
        return valuesFromDatabase.get(key);
    } else {
        String defaultValue = defaultValues.get(key);
        valuesFromDatabase.put(key, defaultvalue);
        return defaultValue;
    }
}

Un'eccezione dovrebbe essere generata solo se non sai come risolvere l'errore localmente.


14
Grazie per la risposta. Per la cronaca, sono questo collega (non mi piaceva il codice originale), ho appena espresso la domanda in modo neutrale in modo che non fosse caricato :)
Konrad Morawski

59
Primo segno di follia: inizi a parlare da solo.
Robert Harvey,

1
@ BЈовић: beh, in quel caso penso che sia ok, ma cosa succede se hai bisogno di due varianti: un metodo per recuperare il valore senza la creazione automatica delle chiavi mancanti e uno con? Quale pensi sia l'alternativa più pulita (e SECCA) allora?
Doc Brown,

3
@RobertHarvey :) Qualcun altro ha creato questo pezzo di codice, una persona veramente separata, ero il recensore
Konrad Morawski,

1
@Alvaro, l'utilizzo delle eccezioni spesso non è un problema. L'uso non corretto delle eccezioni è un problema, indipendentemente dalla lingua.
kdbanman,

11

Non definirei questo uso di Exceptions un anti-pattern, ma non la migliore soluzione al problema di comunicare un risultato complesso.

La soluzione migliore (supponendo che tu sia ancora su Java 7) sarebbe quella di utilizzare Guava's Optional; Non sono d'accordo sul fatto che il suo utilizzo in questo caso sarebbe hacker. Mi sembra, sulla base della spiegazione estesa di Opzionale di Guava , che questo è un esempio perfetto di quando usarlo. Stai facendo una distinzione tra "nessun valore trovato" e "valore di null trovato".


1
Non credo Optionalpossa contenere nulla. Optional.of()accetta solo un riferimento non nullo e Optional.fromNullable()considera null come "valore non presente".
Navin,

1
@Navin non può contenere nulla, ma tu hai Optional.absent()a tua disposizione. E quindi, Optional.fromNullable(string)sarà uguale a Optional.<String>absent()se stringera nullo o Optional.of(string)se non lo fosse.
Konrad Morawski,

@KonradMorawski Ho pensato che il problema nell'OP fosse che non si poteva distinguere tra una stringa nulla e una stringa inesistente senza generare un'eccezione. Optional.absent()copre uno di questi scenari. Come rappresenti l'altro?
Navin,

@Navin con un vero null.
Konrad Morawski,

4
@KonradMorawski: Sembra una pessima idea. Non riesco quasi a pensare a un modo più chiaro per dire "questo metodo non ritorna mai null!" che dichiararlo come ritorno Optional<...>. . .
ruakh,

8

Dal momento che non ci sono considerazioni sulle prestazioni ed è un dettaglio dell'implementazione, alla fine non importa quale soluzione si scelga. Ma devo ammettere che è cattivo stile; l'essere assente chiave è qualcosa che si sa sarà accadere, e non hai nemmeno gestirlo più di una chiamata lo stack, che è dove le eccezioni sono più utili.

L'approccio della tupla è un po 'confuso perché il secondo campo non ha senso quando il valore booleano è falso. Controllare se la chiave esiste in anticipo è sciocco perché la mappa sta cercando la chiave due volte. (Bene, lo stai già facendo, quindi in questo caso tre volte.) La Optionalsoluzione è perfetta per il problema. Potrebbe sembrare un po 'ironico archiviare un nullin un Optional, ma se è quello che l'utente vuole fare, non si può dire di no.

Come notato da Mike nei commenti, c'è un problema con questo; né Guava né Java 8 Optionalconsentono l'archiviazione di messaggi di posta nullelettronica. Quindi avresti bisogno di arrotolare il tuo che, sebbene semplice, comporta una discreta quantità di piastra della caldaia, quindi potrebbe essere eccessivo per qualcosa che verrà utilizzato solo una volta internamente. Puoi anche cambiare il tipo di mappa in Map<String, Optional<String>>, ma gestire Optional<Optional<String>>s diventa imbarazzante.

Un ragionevole compromesso potrebbe essere quello di mantenere l'eccezione, ma riconoscere il suo ruolo di "ritorno alternativo". Creare una nuova eccezione verificata con la traccia dello stack disabilitata e lanciarne un'istanza pre-creata che è possibile conservare in una variabile statica. Questo è economico - probabilmente economico come un ramo locale - e non puoi dimenticare di gestirlo, quindi la mancanza di stack stack non è un problema.


1
Bene, in realtà è solo Map<String, String>a livello di tabella del database, perché la valuescolonna deve essere di un tipo. Tuttavia, esiste un livello DAO wrapper che digita in modo forte ogni coppia chiave-valore. Ma l'ho omesso per chiarezza. (Naturalmente potresti invece avere una tabella a una riga con n colonne, ma quindi aggiungere ogni nuovo valore richiede l'aggiornamento dello schema della tabella, quindi rimuovere la complessità associata al dover analizzarli comporta il costo di introdurre un'altra complessità altrove).
Konrad Morawski,

Un buon punto sulla tupla. Anzi, cosa (false, "foo")dovrebbe significare?
Konrad Morawski,

Almeno con Guava's Optional, cercando di inserire un valore nullo nei risultati in un'eccezione. Dovresti usare Optional.absent ().
Mike Partridge,

@MikePartridge Un buon punto. Una brutta soluzione sarebbe quella di cambiare la mappa in Map<String, Optional<String>>e poi finiresti Optional<Optional<String>>. Una soluzione meno confusa sarebbe quella di creare il proprio Facoltativo che consente nullo un tipo di dati algebrico simile che non consente nullma ha 3 stati: assente, nullo o presente. Entrambi sono semplici da implementare, sebbene la quantità di boilerplate in questione sia elevata per qualcosa che verrà utilizzato solo internamente.
Doval,

2
"Poiché non ci sono considerazioni sulle prestazioni ed è un dettaglio dell'implementazione, alla fine non importa quale soluzione scegliate" - Tranne il fatto che se si utilizza C # utilizzando un'eccezione, si infastidisce chiunque tenti di eseguire il debug con "Interrompi quando viene generata un'eccezione "è attivato per le eccezioni CLR.
Stephen,

5

C'è questa classe Preferenze, che è un secchio per le coppie chiave-valore. I valori nulli sono legali (è importante). Prevediamo che alcuni valori potrebbero non essere ancora stati salvati e vogliamo gestirli automaticamente inizializzandoli con un valore predefinito predefinito quando richiesto.

Il problema è esattamente questo. Ma hai già pubblicato tu stesso la soluzione:

Una variante di questo approccio potrebbe utilizzare Guava's Optional (Guava è già utilizzato in tutto il progetto) invece di qualche Tuple personalizzata, e quindi possiamo distinguere tra Optional.absent () e "proprio" null . Sembra comunque hacker per ragioni che sono facili da vedere - l'introduzione di due livelli di "nullità" sembra abusare del concetto che stava dietro la creazione di Optionals in primo luogo.

Tuttavia, non usare null o Optional . Puoi e dovresti usare Optionalsolo. Per la tua sensazione di "hacking", basta usarlo annidato, quindi si finisce con ciò Optional<Optional<String>>che rende esplicito che potrebbe esserci una chiave nel database (primo livello di opzione) e che potrebbe contenere un valore predefinito (secondo livello di opzione).

Questo approccio è migliore dell'uso delle eccezioni ed è abbastanza facile da capire se Optionalnon è nuovo per te.

Inoltre, tieni presente che Optionalha alcune funzioni di comfort, quindi non devi fare tutto il lavoro da solo. Questi includono:

  • static static <T> Optional<T> fromNullable(T nullableReference)per convertire l'input del database nei Optionaltipi
  • abstract T or(T defaultValue) per ottenere il valore chiave del livello opzionale interno o (se non presente) ottenere il valore chiave predefinito

1
Optional<Optional<String>>sembra malvagio, anche se ha un certo fascino devo ammettere
:)

Puoi spiegare ulteriormente perché sembra malvagio? In realtà è lo stesso modello di List<List<String>>. Ovviamente puoi (se la lingua lo consente) creare un nuovo tipo come quello DBConfigKeyche lo avvolge Optional<Optional<String>>e lo rende più leggibile se lo preferisci.
valenterry,

2
@valenterry È molto diverso. Un elenco di elenchi è un modo legittimo per archiviare alcuni tipi di dati; un'opzione di un'opzione è un modo atipico di indicare due tipi di problemi che i dati potrebbero avere.
Ypnypn,

Un'opzione di un'opzione è come un elenco di elenchi in cui ogni elenco ha uno o nessun elemento. È essenzialmente lo stesso. Solo perché non viene usato spesso in Java non significa che sia intrinsecamente cattivo.
valenterry,

1
@valenterry evil, nel senso che Jon Skeet significa; quindi non proprio male, ma un po 'malvagio, "codice intelligente". Immagino sia solo un po 'barocco, un optional di un opzionale. Non è esplicitamente chiaro quale livello di opzionalità rappresenti cosa. Avrei una doppia interpretazione se l'avessi trovata nel codice di qualcuno. Potresti aver ragione nel ritenere strano solo perché è insolito, unidiomatico. Forse non è niente di speciale per un programmatore di linguaggio funzionale, con le loro monadi e tutto il resto. Anche se non ci proverei io, ho comunque fatto +1 sulla tua risposta perché è interessante
Konrad Morawski,

5

So di essere in ritardo alla festa, ma comunque il tuo caso d'uso ricorda come JavaProperties consente di definire anche un insieme di proprietà predefinite, che verrà controllato se l'istanza non carica alcuna chiave corrispondente.

Osservando come viene eseguita l'implementazione Properties.getProperty(String)(da Java 7):

Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;

Non c'è davvero bisogno di "abusare delle eccezioni per controllare il flusso", come hai citato.

Un approccio simile, ma leggermente più conciso, alla risposta di @BЈовић può anche essere:

public String getByKey(String key) {
    if (!valuesFromDatabase.containsKey(key)) {
        valuesFromDatabase.put(key, defaultValues.get(key));
    }
    return valuesFromDatabase.get(key);
}

4

Anche se penso che la risposta di @BЈовић vada bene nel caso in cui non getValueByKeysia necessario da nessun'altra parte, non penso che la tua soluzione sia sbagliata nel caso in cui il tuo programma contenga entrambi i casi d'uso:

  • recupero tramite chiave con creazione automatica nel caso in cui la chiave non esistesse prima, e

  • recupero senza quell'automatismo, senza cambiare nulla nel database, nel repository o nella mappa chiave (pensate di getValueByKeyessere pubblici, non privati)

Se questa è la tua situazione, e fintanto che il successo delle prestazioni è accettabile, penso che la tua soluzione proposta sia completamente ok. Ha il vantaggio di evitare la duplicazione del codice di recupero, non si basa su framework di terze parti ed è piuttosto semplice (almeno ai miei occhi).

In effetti, in una situazione del genere, dipende dal contesto se una chiave mancante è o meno una situazione "eccezionale". Per un contesto in cui si trova, getValueByKeyè necessario qualcosa di simile . Per un contesto in cui è prevista la creazione automatica di chiavi, fornendo un metodo che riutilizza la funzione già disponibile, ingoia l'eccezione e fornisce un diverso comportamento di errore, ha perfettamente senso. Questo può essere interpretato come un'estensione o "decoratore" di getValueByKey, non tanto come una funzione in cui "l'eccezione viene abusata per il flusso di controllo".

Ovviamente, esiste una terza alternativa: creare un terzo metodo privato restituendo il Tuple<Boolean, String>, come suggerito, e riutilizzare questo metodo sia in getValueByKeyche getByKey. Per un caso più complicato che potrebbe davvero essere l'alternativa migliore, ma per un caso così semplice come mostrato qui, questo ha un odore di ingegnerizzazione eccessiva, e dubito che il codice diventi davvero più gestibile in quel modo. Sono qui con la risposta più in alto qui di Karl Bielefeldt :

"non dovresti sentirti male nell'usare eccezioni quando semplifica il tuo codice".


3

Optionalè la soluzione corretta. Tuttavia, se preferisci, esiste un'alternativa che ha meno di una sensazione di "due null", considera una sentinella.

Definire una stringa statica privata keyNotFoundSentinel, con un valore new String(""). *

Ora il metodo privato può return keyNotFoundSentinelpiuttosto che throw new KeyNotFoundException(). Il metodo pubblico può verificare quel valore

String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
    ... // generate default value
} else {
    return rval;
}

In realtà, nullè un caso speciale di una sentinella. È un valore sentinella definito dalla lingua, con alcuni comportamenti speciali (vale a dire un comportamento ben definito se si chiama un metodo null). Capita solo di essere così utile avere una sentinella come quella che quasi sempre la lingua lo usa.

* Usiamo intenzionalmente new String("")piuttosto che semplicemente ""per impedire a Java di "internare" la stringa, il che le darebbe lo stesso riferimento di qualsiasi altra stringa vuota. Perché ha fatto questo passaggio aggiuntivo, siamo certi che la stringa a cui keyNotFoundSentinelfa riferimento è un'istanza univoca, che dobbiamo garantire che non possa mai apparire nella mappa stessa.


Questa, IMHO, è la risposta migliore.
fgp,

2

Impara dal framework che ha imparato da tutti i punti deboli di Java:

.NET offre due soluzioni molto più eleganti a questo problema, esemplificate da:

Quest'ultimo è molto facile da scrivere in Java, il primo richiede solo una classe di supporto "di riferimento forte".


DictionarySarebbe un'alternativa favorevole alla covarianza al modello T TryGetValue(TKey, out bool).
supercat
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.