Gestione degli errori senza eccezioni

Durante la ricerca di SO per gli approcci alla gestione degli errori relativi alla convalida delle regole aziendali, tutto quello che trovo sono esempi di gestione delle eccezioni strutturate.

MSDN e molte altre risorse di sviluppo affidabili sono molto chiari sul fatto che non si debbano utilizzare eccezioni per gestire casi di errore di routine. Devono essere utilizzati solo per circostanze eccezionali e errori imprevisti che possono verificarsi a causa di un uso improprio da parte del programmatore (ma non dell’utente.) In molti casi, gli errori degli utenti come i campi lasciati vuoti sono comuni e le cose che il nostro programma dovrebbe si aspettano, e quindi non sono eccezionali e non candidati per l’uso di eccezioni.

CITAZIONE:

Ricorda che l’uso del termine eccezione nella programmazione ha a che fare con il pensare che un’eccezione dovrebbe rappresentare una condizione eccezionale. Le condizioni eccezionali, per la loro stessa natura, non si verificano normalmente; quindi il tuo codice non dovrebbe generare eccezioni come parte delle sue operazioni quotidiane.

Non lanciare eccezioni per segnalare eventi che si verificano comunemente. Prendi in considerazione l’utilizzo di metodi alternativi per comunicare a un chiamante il verificarsi di quegli eventi e lasciare il lancio dell’eccezione per quando accade qualcosa di veramente fuori dall’ordinario.

Ad esempio, uso corretto:

private void DoSomething(string requiredParameter) { if (requiredParameter == null) throw new ArgumentExpcetion("requiredParameter cannot be null"); // Remainder of method body... } 

Uso improprio:

 // Renames item to a name supplied by the user. Name must begin with an "F". public void RenameItem(string newName) { // Items must have names that begin with "F" if (!newName.StartsWith("F")) throw new RenameException("New name must begin with /"F/""); // Remainder of method body... } 

Nel caso precedente, secondo le migliori pratiche, sarebbe stato meglio passare l’errore all’interfaccia utente senza coinvolgere / richiedere i meccanismi di gestione delle eccezioni di .NET.

Usando lo stesso esempio sopra, supponiamo che uno debba imporre un insieme di regole di denominazione rispetto agli oggetti. Quale approccio sarebbe meglio?

  1. Avere il metodo restituisce un risultato enumerato? RenameResult.Success, RenameResult.TooShort, RenameResult.TooLong, RenameResult.InvalidCharacters, ecc.

  2. Utilizzo di un evento in una class controller per segnalare alla class UI? L’interfaccia utente chiama il metodo RenameItem del controller e quindi gestisce un evento AfterRename che il controller solleva e che ha lo stato di rinomina come parte degli argomenti dell’evento?

  3. La class di controllo fa direttamente riferimento e chiama un metodo dalla class UI che gestisce l’errore, ad esempio ReportError (testo stringa).

  4. Qualcos’altro… ?

In sostanza, voglio sapere come eseguire la convalida complessa in classi che potrebbero non essere la class Form stessa e riportare gli errori nella class Form per la visualizzazione, ma non voglio coinvolgere la gestione delle eccezioni dove non dovrebbe essere usato (anche se sembra molto più facile!)


Sulla base delle risposte alla domanda, sento che dovrò dichiarare il problema in termini più concreti:

UI = User Interface, BLL = Business Logic Layer (in questo caso, solo una class diversa)

  1. L’utente inserisce il valore nell’interfaccia utente.
  2. L’interfaccia utente riporta valore a BLL.
  3. BLL esegue la convalida di routine del valore.
  4. BLL scopre la violazione delle regole.
  5. BLL restituisce la violazione delle regole all’interfaccia utente.
  6. L’interfaccia utente riceve il ritorno da BLL e segnala l’errore all’utente.

Poiché è normale che un utente inserisca valori non validi, le eccezioni non dovrebbero essere utilizzate. Qual è il modo giusto per farlo senza eccezioni?

L’esempio che date è di input di validazione dell’interfaccia utente.

Pertanto, un buon approccio consiste nel separare la convalida dall’azione. WinForms ha un sistema di convalida integrato, ma in linea di principio funziona in questo modo:

 ValidationResult v = ValidateName(string newName); if (v == ValidationResult.NameOk) SetName(newName); else ReportErrorAndAskUserToRetry(...); 

Inoltre, è ansible applicare la convalida nel metodo SetName per assicurarsi che la validità sia stata verificata:

 public void SetName(string newName) { if (ValidateName(newName) != ValidationResult.NameOk) throw new InvalidOperationException("name has not been correctly validated"); name = newName; } 

(Si noti che questo potrebbe non essere l’approccio migliore per le prestazioni, ma nella situazione dell’applicazione di un semplice controllo di convalida a un input dell’interfaccia utente, è improbabile che la convalida di due volte abbia alcun significato. In alternativa, il controllo di cui sopra potrebbe essere fatto puramente come un controllo di asserzione di debug-only per rilevare qualsiasi tentativo da parte dei programmatori di chiamare il metodo senza prima convalidare l’input.Una volta che sai che tutti i chiamanti rispettano il loro contratto, spesso non è necessario alcun controllo di runtime di rilascio)

Per citare un’altra risposta:

  O un membro adempie al suo contratto o lancia un'eccezione.  Periodo. 

La cosa che manca è: qual è il contratto? È perfettamente ragionevole affermare nel “contratto” che un metodo restituisce un valore di stato. es. File.Exists () restituisce un codice di stato, non un’eccezione, perché questo è il suo contratto.

Tuttavia, il tuo esempio è diverso. In essa, in realtà, esegui due azioni separate: la convalida e l’archiviazione. Se SetName può restituire un codice di stato o impostare il nome, sta tentando di eseguire due attività in una, il che significa che il chiamante non sa mai quale comportamento mostrerà e deve avere una gestione caso speciale per quei casi. Tuttavia, se dividi SetName in passaggi separati Validate e Store, il contratto per StoreName può essere che tu passi in input validi (come passati ValidateName), e genera un’eccezione se questo contratto non viene soddisfatto. Poiché ogni metodo quindi fa una cosa e una sola cosa, il contratto è molto chiaro, ed è ovvio quando deve essere lanciata un’eccezione.

Presumo che tu stia creando il tuo motore di convalida delle regole aziendali, poiché non hai menzionato quello che stai utilizzando.

Userei le eccezioni, ma non le butterei. Ovviamente dovrai accumulare lo stato della valutazione da qualche parte – per registrare il fatto che una determinata regola non è riuscita, vorrei memorizzare un’istanza Exception che descrivesse l’errore. Questo è perché:

  1. Le eccezioni sono serializzabili
  2. Le eccezioni hanno sempre una proprietà Message che è leggibile dall’uomo e può avere proprietà aggiuntive per registrare i dettagli dell’eccezione in forma leggibile dalla macchina.
  3. Alcune delle mancanze delle regole aziendali potrebbero infatti essere state segnalate da eccezioni: una FormatException , ad esempio. Potresti prendere quell’eccezione e aggiungerla alla lista.

In effetti, la rivista MSDN di questo mese contiene un articolo che menziona la nuova class AggregateException in .NET 4.0, che è pensata per essere una raccolta di eccezioni che si sono verificate in un particolare contesto.


Poiché utilizzi Windows Form, devi utilizzare i meccanismi integrati per la convalida: l’evento Validating e il componente ErrorProvider .

Penso che tu abbia avuto l’impressione sbagliata del messaggio previsto. Ecco una grande citazione che ho trovato ieri dall’edizione corrente della rivista Visual Studio (Vol 19, No 8).

O un membro adempie al suo contratto o lancia un’escrizione. Periodo. Nessuna via di mezzo Nessun codice di ritorno, a volte non funziona, a volte no.

Le eccezioni dovrebbero essere utilizzate con caucanvas in quanto sono costose da creare e da buttare, ma sono comunque il modo in cui .NET framework notifica a un client (con questo intendo qualsiasi componente di chiamata) un errore.

Accetto parte del suggerimento di Henk.

Tradizionalmente, le operazioni “pass / fail” sono state implementate come funzioni con un numero intero o un tipo di ritorno bool che avrebbe specificato il risultato della chiamata. Tuttavia, alcuni si oppongono a questo, affermando che “Una funzione o un metodo dovrebbe eseguire un’azione o restituire un valore, ma non entrambi.” In altre parole, un memeber di class che restituisce un valore non dovrebbe essere anche un membro di class che modifica lo stato dell’object.

Ho trovato la soluzione migliore per aggiungere una proprietà .HasErrors / .IsValid e una .Errors all’interno della class che genera gli errori. Le prime due proprietà consentono alla class client di verificare se esistono o meno errori e, se necessario, possono anche leggere la proprietà .Errors e riportare uno o tutti gli errori contenuti. Ogni metodo quindi deve essere consapevole di queste proprietà e gestire lo stato di errore in modo appropriato. Queste proprietà possono quindi essere inserite in un’interfaccia IErrorReporting che possono essere incorporate in varie classi di facciata del livello delle regole aziendali.

A mio parere, in caso di dubbio, lanciare eccezioni sulla convalida delle regole aziendali. So che questo è alquanto controintuitivo e potrei essere fiammeggiato per questo, ma lo sopporto perché ciò che è di routine e ciò che non lo è dipende dalla situazione, ed è una decisione aziendale, non di programmazione.

Ad esempio, se si dispone di classi di dominio aziendali utilizzate da un’app WPF e un servizio WCF, l’input non valido di un campo può essere di routine in un’app WPF, ma sarebbe disastroso quando gli oggetti dominio vengono utilizzati in una situazione WCF in cui stai gestendo richieste di servizio da un’altra applicazione.

Ho pensato a lungo e duramente e ho trovato questa soluzione. Effettuare quanto segue alle classi di dominio:

  • Aggiungi una proprietà: ThrowsOnBusinessRule. Il valore predefinito dovrebbe essere true per generare eccezioni. Impostalo su false se non vuoi lanciarlo.
  • Aggiungi una raccolta di dizionari privata per archiviare le eccezioni con la chiave come proprietà del dominio con violazione delle regole aziendali. (Naturalmente, puoi esporlo pubblicamente se vuoi)
  • Aggiungi un metodo: ThrowsBusinessRule (string propertyName, Exception e) per gestire la logica sopra
  • Se lo desideri, puoi implementare IDataErrorInfo e utilizzare la raccolta di dizionari. L’implementazione di IDataErrorInfo è banale data la configurazione di cui sopra.

Le eccezioni sono proprio questo: un modo per gestire scenari eccezionali, scenari che normalmente non dovrebbero accadere nella tua applicazione. Entrambi gli esempi forniti sono esempi ragionevoli su come utilizzare correttamente le eccezioni. In entrambi i casi, stanno identificando che è stata invocata un’azione che non dovrebbe essere consentita e che è eccezionale per il normale stream dell’applicazione.

L’errata interpretazione è che il secondo errore, il metodo di ridenominazione, è l’ unico meccanismo per rilevare l’errore di ridenominazione. Le eccezioni non dovrebbero mai essere utilizzate come meccanismo per il passaggio di messaggi a un’interfaccia utente. In questo caso, avresti qualche controllo logico sul nome specificato per la rinomina che è valido da qualche parte nella convalida dell’interfaccia utente. Questa convalida farebbe in modo che l’eccezione non faccia mai parte del stream normale.

Le eccezioni sono lì per fermare “cose ​​brutte” che accadono, fungono da ultima linea di difesa per le chiamate API per garantire che gli errori vengano interrotti e che solo il comportamento legale possa aver luogo. Sono errori a livello di programmatore e dovrebbero sempre indicare solo uno dei seguenti eventi:

  • Si è verificato qualcosa di catastrofico e sistemico, come l’esaurimento della memoria.
  • Un programmatore è andato e ha programmato qualcosa di sbagliato, sia che si tratti di una ctriggers chiamata a un metodo o di un pezzo di SQL illegale.

Non dovrebbero essere l’unica linea di difesa contro l’errore dell’utente. Gli utenti richiedono molto più feedback e assistenza di quanto un’eccezione non offrirà mai e tenteranno di solito di fare cose che non rientrano nel stream previsto delle applicazioni.