Parametro per controllare se generare un'eccezione o restituire null - buona pratica?


24

Mi capita spesso di imbattermi in metodi / funzioni che hanno un parametro booleano aggiuntivo che controlla se viene generata un'eccezione in caso di errore o viene restituito null.

Ci sono già discussioni su quale di queste sia la scelta migliore in quale caso, quindi non concentriamoci su questo qui. Vedi ad es. Restituisci valore magico, lancia eccezioni o restituisci falso in caso di fallimento?

Supponiamo invece che ci sia una buona ragione per cui vogliamo sostenere entrambi i modi.

Personalmente penso che un tale metodo dovrebbe piuttosto essere diviso in due: uno che genera un'eccezione in caso di fallimento, l'altro che restituisce null in caso di fallimento.

Quindi, qual è la migliore?

A: Un metodo con $exception_on_failureparametro.

/**
 * @param int $id
 * @param bool $exception_on_failure
 *
 * @return Item|null
 *   The item, or null if not found and $exception_on_failure is false.
 * @throws NoSuchItemException
 *   Thrown if item not found, and $exception_on_failure is true.
 */
function loadItem(int $id, bool $exception_on_failure): ?Item;

B: Due metodi distinti.

/**
 * @param int $id
 *
 * @return Item|null
 *   The item, or null if not found.
 */
function loadItemOrNull(int $id): ?Item;

/**
 * @param int $id
 *
 * @return Item
 *   The item, if found (exception otherwise).
 *
 * @throws NoSuchItemException
 *   Thrown if item not found.
 */
function loadItem(int $id): Item;

EDIT: C: Qualcos'altro?

Molte persone hanno suggerito altre opzioni o affermano che sia A che B sono difettosi. Tali suggerimenti o opinioni sono ben accetti, pertinenti e utili. Una risposta completa può contenere tali informazioni extra, ma affronterà anche la domanda principale se un parametro per modificare la firma / il comportamento sia una buona idea.

Gli appunti

Nel caso qualcuno si stia chiedendo: gli esempi sono in PHP. Ma penso che la domanda si applichi in tutte le lingue purché siano in qualche modo simili a PHP o Java.


5
Mentre devo lavorare in PHP e sono abbastanza abile, è semplicemente un brutto linguaggio e la sua libreria standard è dappertutto. Leggi eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design per riferimento. Quindi non è davvero sorprendente che alcuni sviluppatori PHP adottino cattive abitudini. Ma non ho ancora visto questo particolare odore di design.
Polygnome il

Mi manca un punto essenziale nelle risposte fornite: quando useresti l'uno o l'altro. Utilizzeresti il ​​metodo dell'eccezione nel caso in cui un elemento non trovato fosse inatteso e considerato un errore (tempo di panico). Utilizzeresti il ​​metodo Try ... nel caso in cui un articolo mancante non sia improbabile e certamente non sia un errore ma solo una possibilità da includere nel normale controllo del flusso.
Martin Maat,


1
@Polygnome Hai ragione, le funzioni di libreria standard sono ovunque, e esiste un sacco di codice scadente, e c'è il problema di un processo per richiesta. Ma sta migliorando con ogni versione. I tipi e il modello a oggetti fanno tutte o quasi tutte le cose che ci si può aspettare da esso. Voglio vedere generici, co-e contravarianza, tipi di callback che specificano una firma, ecc. Molto di questo è in discussione o sulla strada da implementare. Penso che la direzione generale sia buona, anche se le stranezze della libreria standard non andranno via facilmente.
donquixote,

4
Un suggerimento: invece di loadItemOrNull(id)considerare se loadItemOr(id, defaultItem)ha senso per te. Se l'elemento è una stringa o un numero, lo fa spesso.
user949300

Risposte:


35

Hai ragione: due metodi sono molto migliori per questo, per diversi motivi:

  1. In Java, la firma del metodo che potenzialmente genera un'eccezione includerà questa eccezione; l'altro metodo no. Rende particolarmente chiaro cosa aspettarsi dall'uno e dall'altro.

  2. In linguaggi come C # in cui la firma del metodo non dice nulla delle eccezioni, i metodi pubblici dovrebbero essere comunque documentati e tale documentazione include le eccezioni. Documentare un singolo metodo non sarebbe facile.

    Il tuo esempio è perfetto: i commenti nel secondo pezzo di codice sembrano molto più chiari, e vorrei anche abbreviare "L'elemento, se trovato (eccezione altrimenti)" fino a "L'elemento". - La presenza di una potenziale eccezione e il la descrizione che ci hai dato è autoesplicativa.

  3. Nel caso di un singolo metodo, ci sono alcuni casi in cui si desidera attivare o disattivare il valore del parametro booleano in fase di esecuzione e, in tal caso, ciò significa che il chiamante dovrà gestire entrambi i casi (una nullrisposta e l'eccezione ), rendendo il codice molto più difficile di quanto debba essere. Poiché la scelta non viene effettuata in fase di esecuzione, ma quando si scrive il codice, due metodi hanno perfettamente senso.

  4. Alcuni framework, come .NET Framework, hanno stabilito convenzioni per questa situazione e lo risolvono con due metodi, proprio come hai suggerito. L'unica differenza è che usano uno schema spiegato da Ewan nella sua risposta , quindi int.Parse(string): intgenera un'eccezione, mentre int.TryParse(string, out int): boolnon lo fa. Questa convenzione di denominazione è molto forte nella comunità di .NET e dovrebbe essere seguita ogni volta che il codice corrisponde alla situazione che descrivi.


1
Grazie, questa è la risposta che stavo cercando! Lo scopo era iniziare una discussione su questo per Drupal CMS, drupal.org/project/drupal/issues/2932772 , e non voglio che ciò riguardi le mie preferenze personali, ma eseguo il backup con feedback da altri.
donquixote,

4
In realtà una tradizione di lunga data per almeno .NET non prevede l'uso di due metodi, ma invece la scelta giusta per l'attività giusta. Eccezioni sono per circostanze eccezionali, per non gestire il flusso di controllo previsto. Non riesci ad analizzare una stringa in un numero intero? Non una circostanza molto inaspettata o eccezionale. int.Parse()è considerata una pessima decisione di progettazione, motivo per cui il team di .NET ha proseguito e implementato TryParse.
Voo,

1
Fornire due metodi invece di pensare a quale tipo di caso d'uso stiamo affrontando, è la via d'uscita facile: non pensare a quello che stai facendo, invece affidare la responsabilità a tutti gli utenti della tua API. Certo che funziona, ma non è questo il segno distintivo di un buon design API (o di qualsiasi progetto per la questione. Leggi ad esempio Joel lo prende su questo .
Voo

@Voo Punto valido. Effettivamente fornire due metodi pone la scelta sul chiamante. Forse per alcuni valori di parametro dovrebbe esistere un elemento, mentre per altri valori di parametro un elemento inesistente è considerato un caso normale. Forse il componente viene utilizzato in un'applicazione in cui è sempre previsto che esistano gli articoli e in un'altra applicazione in cui gli elementi inesistenti sono considerati normali. Forse il chiamante chiama ->itemExists($id)prima di chiamare ->loadItem($id). O forse è solo una scelta fatta da altre persone in passato, e dobbiamo darlo per scontato.
donquixote,

1
Motivo 1b: alcune lingue (Haskell, Swift) richiedono la nullità per essere presenti nella firma. In particolare, Haskell ha un tipo completamente diverso per i valori nullable ( data Maybe a = Just a | Nothing).
John Dvorak,

9

Una variante interessante è il linguaggio Swift. In Swift, si dichiara una funzione come "lancio", ovvero è consentito lanciare errori (simile ma non esattamente uguale a dire eccezioni Java). Il chiamante può chiamare questa funzione in quattro modi:

un. In un'istruzione try / catch che rileva e gestisce l'eccezione.

b. Se il chiamante viene dichiarato come lancio, chiamalo e ogni errore generato si trasforma in un errore generato dal chiamante.

c. Contrassegnare la chiamata di funzione con il try!significato di "Sono sicuro che questa chiamata non verrà lanciata". Se la funzione chiamata fa lancio, l'applicazione è garantita per incidente.

d. Contrassegnare la chiamata di funzione con il try?significato di "So che la funzione può essere lanciata ma non mi interessa quale errore viene lanciato esattamente". Ciò modifica il valore restituito della funzione dal tipo " T" al tipo " optional<T>" e un'eccezione generata viene restituita nulla.

(a) e (d) sono i casi che stai chiedendo. Cosa succede se la funzione può restituire zero e generare eccezioni? In tal caso il tipo del valore restituito sarebbe " optional<R>" per un certo tipo R, e (d) lo cambierebbe in " optional<optional<R>>" che è un po 'criptico e difficile da capire ma del tutto soddisfacente. E una funzione Swift può tornare optional<Void>, quindi (d) può essere usata per lanciare funzioni che restituiscono Void.


2

Mi piace l' bool TryMethodName(out returnValue)approccio.

Ti dà un'indicazione conclusiva se il metodo ha avuto successo o meno e la denominazione corrisponde al wrapping del metodo di lancio dell'eccezione in un blocco try catch.

Se si restituisce null, non si sa se è fallito o se il valore restituito era legittimamente nullo.

per esempio:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

esempio di utilizzo (out significa passare per riferimento non inizializzato)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}

Scusa, mi hai perso qui. Come sarebbe il tuo " bool TryMethodName(out returnValue)approccio" nell'esempio originale?
donquixote,

modificato per aggiungere un esempio
Ewan il

1
Quindi usi un parametro per riferimento invece di un valore di ritorno, quindi hai il valore di ritorno libero per il successo bool? Esiste un linguaggio di programmazione che supporta questa parola chiave "out"?
donquixote,

2
sì, mi spiace non aver capito che "out" è specifico per c #
Ewan il

1
non sempre no. E se l'articolo 3 fosse nullo? non sapresti se si è verificato un errore oppure no
Ewan il

2

Di solito un parametro booleano indica che una funzione può essere suddivisa in due e, di regola, è sempre necessario suddividerla anziché passare in un valore booleano.


1
Non lo escluderei di norma senza aggiungere qualifiche o sfumature. Rimuovere un booleano come argomento può costringerti a scrivere un muto if / else in qualche altro posto e quindi codificare i dati nel tuo codice scritto e non in variabili.
Gerard,

@Gerard puoi farmi un esempio per favore?
Max

@Max Quando hai una variabile booleana b: invece di chiamare foo(…, b);devi scrivere if (b) then foo_x(…) else foo_y(…);e ripetere gli altri argomenti, o qualcosa del genere ((b) ? foo_x : foo_y)(…)se la lingua ha un operatore ternario e funzioni / metodi di prima classe. E questo potrebbe anche ripetersi da diversi punti del programma.
BlackJack,

@ Black Jack, sì, potrei essere d'accordo con te. Ho pensato all'esempio, if (myGrandmaMadeCookies) eatThem() else makeFood()che sì, potrei includere in un'altra funzione come questa checkIfShouldCook(myGrandmaMadeCookies). Mi chiedo se la sfumatura che hai menzionato abbia a che fare con se stiamo passando un booleano su come vogliamo che la funzione si comporti, come nella domanda originale, rispetto a passare alcune informazioni sul nostro modello, come nel esempio della nonna che ho appena dato.
Max

1
La sfumatura di cui al mio commento si riferisce a "dovresti sempre". Magari riformulalo come "se a, b e c sono applicabili allora [...]". La "regola" che citi è un grande paradigma, ma molto variante nel caso d'uso.
Gerard,

1

Clean Code consiglia di evitare argomenti con flag booleani, poiché il loro significato è opaco nel sito di chiamata:

loadItem(id, true) // does this throw or not? We need to check the docs ...

mentre metodi separati possono usare nomi espressivi:

loadItemOrNull(id); // meaning is obvious; no need to refer to docs

1

Se dovessi usare A o B, utilizzerei B, due metodi separati, per i motivi indicati in risposta di Arseni Mourzenkos .

Ma c'è un altro metodo 1 : in Java viene chiamato Optional, ma è possibile applicare lo stesso concetto ad altre lingue in cui è possibile definire i propri tipi, se la lingua non fornisce già una classe simile.

Definisci il tuo metodo in questo modo:

Optional<Item> loadItem(int id);

Nel tuo metodo ritorni Optional.of(item)se hai trovato l'articolo o ritorni Optional.empty()in caso contrario. Non lanci mai 2 e non ritorni mai null.

Per il cliente del tuo metodo è qualcosa di simile null, ma con la grande differenza che costringe a pensare al caso degli oggetti mancanti. L'utente non può semplicemente ignorare il fatto che potrebbe non esserci alcun risultato. Per estrarre l'elemento da Optionalun'azione esplicita è necessario:

  • loadItem(1).get()lancerà NoSuchElementExceptiono restituirà l'oggetto trovato.
  • C'è anche loadItem(1).orElseThrow(() -> new MyException("No item 1"))da usare un'eccezione personalizzata se il reso Optionalè vuoto.
  • loadItem(1).orElse(defaultItem)restituisce l'oggetto trovato o il passato defaultItem(che può anche essere null) senza generare un'eccezione.
  • loadItem(1).map(Item::getName)restituirebbe nuovamente un Optionalcon il nome degli elementi, se presente.
  • Ci sono altri metodi, vedi Javadoc perOptional .

Il vantaggio è che ora spetta al codice client cosa dovrebbe succedere se non ci sono articoli e devi solo fornire un singolo metodo .


1 Immagino sia qualcosa di simile alla rispostatry? in gnasher729s ma non mi è del tutto chiaro in quanto non conosco Swift e sembra essere una funzionalità linguistica, quindi è specifica per Swift.

2 È possibile generare eccezioni per altri motivi, ad esempio se non si sa se esiste un elemento o meno perché si sono verificati problemi di comunicazione con il database.


0

Come altro punto d'accordo con l'utilizzo di due metodi, questo può essere molto facile da implementare. Scriverò il mio esempio in C #, poiché non conosco la sintassi di PHP.

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}

0

Preferisco due metodi, in questo modo, non solo mi dirai che stai restituendo un valore, ma quale valore esattamente.

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething () non è neanche un cattivo modello.

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

Sono più abituato a vedere il primo nelle interazioni con il database mentre il secondo è più usato nelle analisi.

Come altri hanno già detto, i parametri flag sono di solito un odore di codice (odori di codice non significano che stai facendo qualcosa di sbagliato, solo che c'è una probabilità che tu lo stia sbagliando). Sebbene tu lo chiami proprio, a volte ci sono sfumature che rendono difficile sapere come usare quella bandiera e finisci per guardare all'interno del corpo del metodo.


-1

Mentre altre persone suggeriscono il contrario, suggerirei che avere un metodo con un parametro per indicare come gestire gli errori è meglio che richiedere l'uso di metodi separati per i due scenari di utilizzo. Mentre può essere desiderabile avere anche punti di ingresso distinti per gli utilizzi di "errore genera" rispetto a "errore restituisce l'indicazione di errore", avere un punto di ingresso in grado di servire entrambi i modelli di utilizzo eviterà la duplicazione del codice nei metodi wrapper. A titolo di esempio, se uno ha delle funzioni:

Thing getThing(string x);
Thing tryGetThing (string x);

e si vogliono funzioni che si comportano allo stesso modo, ma con x racchiuso tra parentesi, sarebbe necessario scrivere due set di funzioni wrapper:

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

Se esiste un argomento su come gestire gli errori, sarebbe necessario un solo wrapper:

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

Se il wrapper è abbastanza semplice, la duplicazione del codice potrebbe non essere troppo negativa, ma se potrebbe mai essere necessario modificarla, la duplicazione del codice richiederà a sua volta la duplicazione di eventuali modifiche. Anche se si sceglie di aggiungere punti di ingresso che non utilizzano l'argomento aggiuntivo:

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

si può mantenere tutta la "logica aziendale" in un solo metodo, quello che accetta un argomento che indica cosa fare in caso di fallimento.


Hai ragione: i decoratori e i wrapper e persino l'implementazione regolare avranno una duplicazione del codice quando si usano due metodi.
donquixote,

Metti sicuramente le versioni con e senza lancio di eccezioni in pacchetti separati e rendi generici i wrapper?
Will Crawford,

-1

Penso che tutte le risposte che spiegano che non si dovrebbe avere una bandiera che controlli se viene generata o meno un'eccezione siano valide. Il loro consiglio sarebbe il mio consiglio a chiunque senta la necessità di porre quella domanda.

Volevo tuttavia sottolineare che esiste un'eccezione significativa a questa regola. Una volta che sei abbastanza avanzato per iniziare a sviluppare API che centinaia di altre persone useranno, ci sono casi in cui potresti voler fornire una tale bandiera. Quando scrivi un'API, non stai solo scrivendo ai puristi. Stai scrivendo a clienti reali che hanno desideri reali. Parte del tuo lavoro è renderli felici con la tua API. Questo diventa un problema sociale, piuttosto che un problema di programmazione, e talvolta i problemi sociali dettano soluzioni che sarebbero meno che ideali come soluzioni di programmazione da sole.

Potresti scoprire di avere due utenti distinti della tua API, uno dei quali richiede eccezioni e uno che non lo fa. Un esempio di vita reale in cui ciò potrebbe accadere è con una libreria utilizzata sia nello sviluppo che su un sistema incorporato. Negli ambienti di sviluppo, gli utenti vorranno probabilmente avere eccezioni ovunque. Le eccezioni sono molto popolari per la gestione di situazioni impreviste. Tuttavia, sono vietati in molte situazioni incorporate perché sono troppo difficili da analizzare i vincoli in tempo reale. Quando in realtà ti preoccupi non solo del tempo medio impiegato per eseguire la tua funzione, ma del tempo necessario per qualsiasi divertimento individuale, l'idea di svolgere la pila in qualsiasi luogo arbitrario è molto indesiderabile.

Un esempio di vita reale per me: una biblioteca di matematica. Se la tua libreria matematica ha una Vectorclasse che supporta ottenere un vettore di unità nella stessa direzione, devi essere in grado di dividere per grandezza. Se la grandezza è 0, hai una divisione per zero. In fase di sviluppo , vuoi catturarli . Non vuoi davvero una divisione sorprendente per gli 0 in giro. Lanciare un'eccezione è una soluzione molto popolare per questo. Non succede quasi mai (è un comportamento davvero eccezionale), ma quando lo fa, lo vuoi sapere.

Sulla piattaforma integrata, non si desidera tali eccezioni. Avrebbe più senso fare un controllo if (this->mag() == 0) return Vector(0, 0, 0);. In effetti, lo vedo nel vero codice.

Ora pensa dal punto di vista di un'azienda. Puoi provare a insegnare due modi diversi di utilizzare un'API:

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

Questo soddisfa le opinioni della maggior parte delle risposte qui, ma dal punto di vista dell'azienda questo è indesiderabile. Gli sviluppatori incorporati dovranno imparare uno stile di codifica e gli sviluppatori dello sviluppo dovranno imparare un altro. Questo può essere piuttosto fastidioso e costringe le persone a un solo modo di pensare. Potenzialmente peggio: come si fa a dimostrare che la versione incorporata non include alcun codice di lancio delle eccezioni? La versione di lancio può facilmente confondersi, nascondendosi da qualche parte nel codice fino a quando non viene trovata da un costoso Failure Review Board.

Se invece hai una bandiera che attiva o disattiva la gestione delle eccezioni, entrambi i gruppi possono apprendere lo stesso identico stile di codifica. In effetti, in molti casi, puoi persino usare il codice di un gruppo nei progetti dell'altro gruppo. Nel caso in cui utilizzi questo codice unit(), se in realtà ti interessa cosa succede in un caso di divisione per 0, dovresti aver scritto tu stesso il test. Pertanto, se lo si sposta su un sistema incorporato, dove si ottengono solo risultati negativi, tale comportamento è "sano di mente". Ora, dal punto di vista aziendale, alleno i miei programmatori una volta e usano le stesse API e gli stessi stili in tutte le parti della mia attività.

Nella stragrande maggioranza dei casi, vuoi seguire i consigli degli altri: usa modi di tryGetUnit()dire simili o simili per funzioni che non generano eccezioni e restituisce invece un valore sentinella come null. Se ritieni che gli utenti potrebbero voler utilizzare sia il codice di gestione delle eccezioni sia il codice di gestione delle eccezioni fianco a fianco all'interno di un singolo programma, segui la tryGetUnit()notazione. Tuttavia, esiste un caso angolare in cui la realtà degli affari può rendere migliore l'uso di una bandiera. Se ti trovi in ​​quel caso d'angolo, non aver paura di lanciare le "regole" a margine. Ecco a cosa servono le regole!

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.