Eccezioni vs ErrorCodes quando si lavora con i dispositivi

4

Il team esterno è al capolinea di un nuovo progetto. Uno dei componenti al confine del sistema è il componente che interagisce con una stampante attraverso un componente COM esterno (indicato come una normale dll).

Il componente COM restituisce i codici interi se si è verificato un errore durante l'esecuzione del comando. Consideriamo un codice concreto.

  public class OperationException:Exception {
    public int ErrorCode { get; private set; }
    public string ErrorDescription { get; private set; }

    public OperationException(int errorCode, string errorDescription) {
        ErrorCode = errorCode;
        ErrorDescription = errorDescription;
    }
}

//The exception throwing way
public class Provider1:IFrProvider {
    private readonly IDrvFR48 driver;

    public string GetSerialNumber() {
        //must read status before get SerialNumber
        int resultCode = driver.ReadEcrStatus();
        if (resultCode != 0) {
            throw new OperationException(resultCode, driver.ErrorDescription);
        }
        return driver.SerialNumber;
    }
}

//The way of out parameters returning.
public class Provider2 : IFrProvider
{
    private readonly IDrvFR48 driver;      

    public string GetSerialNumber(out Result result) {
        //must read status before get SerialNumber
        int resultCode = driver.ReadEcrStatus();           
        if (resultCode != 0) {
            result = new Result(resultCode, driver.ErrorDescription);
            return null;
        }
        result = new Result(0, null);
        return driver.SerialNumber;
    }
}

//The way of LastResult property setting.
public class Provider3 : IFrProvider
{
    private readonly IDrvFR48 driver;

    public Result LastResult {
        get {
            return new Result(driver.ErrorCode, driver.ErrorDescription);
        }
    }   

    public string GetSerialNumber() {
        //must read status before get SerialNumber
        if (driver.GetECRStatus() == 0)
            return driver.SerialNumber;
        return null;
    }
}

public class Result {
    public int ResultCode { get; private set; }
    public string Description { get; private set; }

    public Result(int resultCode, string description) {
        ResultCode = resultCode;
        Description = description;
    }
}

public class Caller {
    public void CallProvider1() {
        var provider = new Provider1();
        try {
            string serialNumber = provider.GetSerialNumber();
            //success flow
        }
        catch (OperationException e) {
            if (e.ErrorCode == 123) {
                //handling logic
            }
        }
    }

    public void CallProvider2() {
        var provider = new Provider2();

        Result result;
        string serialNumber = provider.GetSerialNumber(out result);
        if (result.ResultCode == 123) {
            //error handling   
        }
        //success flow
    }

    public void CallProvider3()
    {
        var provider = new Provider3();

        string serialNumber = provider.GetSerialNumber();
        if (provider.LastResult.ResultCode == 123) {                
            //handling               
        }
        //success flow                
    }
}

Quindi abbiamo tre modi di gestire gli errori. La cosa più specifica di tutte queste cose è che ogni operazione può fallire, perché dipende da un dispositivo.

Il nostro team pensa di utilizzare i parametri, a causa della paura della propagazione delle eccezioni non gestite. L'altro motivo, collegato alla paura, è che dovremo coprire tutte le chiamate provando \ catch i blocchi per non avere paura, ma un tale codice sarà piuttosto brutto.

Quali pro e contro vedi e cosa potresti dare un consiglio?

    
posta EngineerSpock 03.08.2013 - 18:45
fonte

4 risposte

2

La paura con eccezione può essere facilitata imparando lo stile di programmazione della gestione delle eccezioni (e anche le tecniche generali di gestione degli errori).

La corretta gestione degli errori (applicabile a tutti i meccanismi e particolarmente applicabile alla gestione delle eccezioni) richiede un ragionato ragionamento sulle conseguenze e l'interpretazione di ogni operazione fallita. Tale ragionamento implica:

  1. Dopo aver riscontrato un errore, quale linea di codice verrà eseguita successivamente?
  2. Dopo aver riscontrato un errore, cosa dice sullo stato corrente del dispositivo?
    • Il dispositivo può riprendere il normale funzionamento?
    • Il dispositivo ha bisogno di un reset?
    • L'utente umano deve essere avvisato?
  3. Dopo aver riscontrato un errore, lo stato del programma è ancora coerente?
    • Il programma dovrebbe fare un po 'di pulizia per ripristinare la consistenza dello stato del programma?

Questo ragionamento può essere esteso a qualsiasi black box di programmazione come una libreria di terze parti.

Esempio di codice.

Per garantire che ogni chiamata riuscita a QueryStatusStart sia seguita da una chiamata a QueryStatusFinish con altre chiamate intermedie:

bool queryStatusStarted = false;
try
{
    // throws exception if fails
    device.QueryStatusStart(); 

    // this line is not executed unless the previous line succeeded.
    queryStatusStarted = true; 

    // might throw exception
    device.MakeMoreQueryStatusCalls(); 
}
finally 
{
    // Whether or note MakeMoreQueryStatusCalls() throws an exception,
    // we will reach here. Hence we can restore the device to a 
    // known state.

    if (queryStatusStarted)
    {     
        device.QueryStatusFinish();  
    } 
}

Nella programmazione generale, i metodi di pulizia dello stato (come il metodo QueryStatusFinish sopra) non dovrebbe lanciare un'eccezione (perché si genera un'eccezione in una presa {} o, infine, il blocco {} causerà effettivamente l'eccezione non rilevata problema che hai temuto.

Tuttavia, quando si ha a che fare con un dispositivo fisico, c'è un bisogno di interpretare cosa una pulizia fallita (ripristino) significa:

  1. Il dispositivo è stato disconnesso
  2. Il dispositivo ha malfunzionamenti e richiede l'intervento umano (come il ripristino dell'alimentazione)

In entrambi i casi, il programma non sarà in grado di continuare operazione normale. Il modo corretto di gestire questa situazione varia da caso a caso.

    
risposta data 03.08.2013 - 23:43
fonte
6

Non vedo che usare i codici di errore ti porterà qualcosa oltre le eccezioni. È altrettanto facile perdere il controllo di un codice di errore, e poi?

Inoltre, considera il seguente esempio:

public void CallProvider2() {
    var provider = new Provider2();

    Result result;
    string serialNumber = provider.GetSerialNumber(out result);
    if (result.ResultCode != 0) {
        //error handling
        switch(result.ResultCode)
        { 
            case 123:
            break;
            //more cases
            default:
                //what now?
            break;
        }
    }
    //success flow
}

Quanto sopra è come potresti gestire gli errori, e il caso "predefinito" prenderebbe i codici di errore non controllati e sarebbe l'equivalente di un'eccezione non gestita. Varrebbe la pena di chiedere cosa si dovrebbe fare se il caso viene mai raggiunto. Non puoi continuare, ma devi uscire in sicurezza da quel metodo o cosa? Stai solo andando a restituire l'oggetto Risultato fino in cima allo stack e controlla se ResultCode! = 0 ovunque? Tanto vale attenersi alle eccezioni.

Altri vantaggi delle eccezioni da considerare:

1) Ottieni automaticamente una traccia dello stack, che può essere enormemente utile nel debugging.

2) Se è necessario gestire un errore più in alto nello stack di chiamate, le eccezioni si propagano automaticamente verso l'alto. Puoi anche gestirli e rilanciarli. Negli esempi ResultCode dovresti scrivere un codice aggiuntivo per passarli se necessario.

3) I progetti di runtime .NET presentano alcune variazioni di un evento di UnhandledException a cui è possibile iscriversi e effettuare una registrazione degli errori centralizzata. Anche se la tua app fallisce, puoi comunque ottenere alcune informazioni sul perché. L'impianto idraulico per farlo è già integrato, devi solo collegarlo ad esso.

4) Un arresto anomalo dell'app da un'eccezione non gestita, sebbene indesiderabile, è UTILE. Nessuno può confondere che qualcosa è andato storto.

    
risposta data 04.08.2013 - 01:26
fonte
3

Domanda molto discutibile, che dipende da molte condizioni del tuo progetto, flusso di lavoro, se il tuo codice sarà usato da altri sviluppatori, ecc.

Sì, la pratica .Net comune consiste nel generare eccezioni invece di restituire un codice di errore. Ottimo commento a riguardo qui .

Error codes are old skool, you needed them back in the bad old days of COM and C programming, environments that didn't support exceptions. Nowadays, you use exceptions to notify client code or the user about problems. Exceptions in .NET are self-descriptive, they have a type, a message and diagnostics.

Pensato, devi avere in mente un altro aspetto - se il codice dell'errore stesso è anche auto-descrittivo (come i codici http, 404, 503, ecc.), allora puoi restituire il codice. E il framework .Net restituisce effettivamente tale codice con HttpWebResponse.StatusCode ( anche se ancora avvolto da enum). Ma fatelo se sapete per certo (he-he) che questi codici non cambieranno in futuro.

In altri casi, che senso ha l'utente del tuo codice per sapere che era -lets dice - errore # 23452, che si è verificato. Inoltre, può essere modificato in futuro, che fa sì che l'utente esegua l'override del suo codice a causa del numero modificato.

    
risposta data 03.08.2013 - 21:52
fonte
0

Quando si progetta un'API per operazioni che potrebbero fallire a causa di ragioni prevedibili, ma a tempi imprevedibili, si dovrebbe generalmente consentire un mezzo attraverso il quale i chiamanti possono indicare se si aspettano di far fronte ai fallimenti semi-prevedibili. Se gli unici metodi disponibili sono restituiti con i codici di errore piuttosto che con le eccezioni, a ogni sito di chiamata verrà richiesto di verificare il valore di ritorno, anche se l'unica cosa che può fare con un codice di errore è generare un'eccezione. Se gli unici metodi disponibili generano eccezioni per tutti i fallimenti, anche quelli che un chiamante potrebbe aspettarsi, allora ogni chiamante che sarebbe pronto a gestire un errore dovrà usare un try / catch per quello scopo.

Un mezzo comune per esporre i metodi i cui chiamanti possono o meno aspettarsi errori è quello di avere coppie di metodi che falsificano il nome, ad esempio TryFnorble e Fnorble . Supponi che Thing1a e Thing1b abbiano entrambi gli stili di metodo, mentre Thing2a ha solo il primo e Thing2b ha solo il secondo. Il codice che è pronto a gestire un problema con la prima cosa ma non il secondo potrebbe essere scritto come:

Thing1a.Fnorble();
if (Thing1b.Fnorble())
  Thing1bIsReady = true;
else
  Thing1bIsReady = false;

Al contrario, se il codice utilizza Thing2a / Thing2b , dovrebbe essere scritto come:

if (!Thing2a.Fnorble())
  throw new FnorbleFailureException(...);
try
{
   Thing2b.Fnorble();
   Thing2bIsReady = true;
}
catch (Ex as FnorbleFailureException)
{
   Thing2bIsReady = False;
}

Molto meno bello.

[Nota: utilizzo if/else e imposta / svuota esplicitamente il flag Thing1bIsReady , anziché assegnare semplicemente il valore di ritorno direttamente al flag, perché anche se il tipo di ritorno e il flag sono entrambi bool , I li considererebbero avere significati semanticamente diversi. Prenderò in considerazione il fatto che avere il metodo return true una volta farebbe sì che il flag sia impostato immediatamente, e averlo restituito false una volta farebbe sì che il flag venga cancellato immediatamente come dettaglio di implementazione.

    
risposta data 09.07.2014 - 21:54
fonte