Domanda

Sono interessato a sapere quali tecniche stai utilizzando per convalidare lo stato interno di un oggetto durante un'operazione che, dal suo punto di vista, può fallire solo a causa di cattivo stato interno o violazione invariante.

Il mio focus principale è su C ++, poiché in C # il modo ufficiale e prevalente è quello di gettare un'eccezione, e in C ++ non c'è solo un singolo per farlo (ok, non proprio in C # neanche io lo so).

Nota che non sto non parlando della convalida dei parametri di funzione, ma più simile ai controlli di integrità invarianti della classe.

Ad esempio, supponiamo di voler un oggetto Printer su Queue un lavoro di stampa in modo asincrono. Per l'utente di Printer , tale operazione può avere esito positivo solo perché un risultato di coda asincrono con arriva in un altro momento. Pertanto, non esiste un codice di errore rilevante da comunicare al chiamante.

Ma per l'oggetto Printer , questa operazione può fallire se lo stato interno è cattivo, cioè l'invariante di classe è rotta, il che significa sostanzialmente: un bug. Questa condizione non è necessariamente di alcun interesse per l'utente dell'oggetto Printer .

Personalmente, tendo a mescolare tre stili di convalida dello stato interno e non riesco davvero a decidere quale sia il migliore, se del caso, solo qual è il peggiore. Mi piacerebbe sentire le tue opinioni su questi e anche che condividi le tue esperienze e pensieri su questo argomento.

Il primo stile che uso - meglio fallire in modo controllabile rispetto ai dati corrotti:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in both release and debug builds.
    // Never proceed with the queuing in a bad state.
    if(!IsValidState())
    {
        throw InvalidOperationException();
    }

    // Continue with queuing, parameter checking, etc.
    // Internal state is guaranteed to be good.
}

Il secondo stile che uso - migliore crash incontrollabile rispetto ai dati corrotti:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in debug builds only.
    // Break into the debugger in debug builds.
    // Always proceed with the queuing, also in a bad state.
    DebugAssert(IsValidState());

    // Continue with queuing, parameter checking, etc.
    // Generally, behavior is now undefined, because of bad internal state.
    // But, specifically, this often means an access violation when
    // a NULL pointer is dereferenced, or something similar, and that crash will
    // generate a dump file that can be used to find the error cause during
    // testing before shipping the product.
}

Il terzo stile che uso - meglio salvare silenziosamente e in modo difensivo rispetto ai dati corrotti:

void Printer::Queue(const PrintJob& job)
{
    // Validate the state in both release and debug builds.
    // Break into the debugger in debug builds.
    // Never proceed with the queuing in a bad state.
    // This object will likely never again succeed in queuing anything.
    if(!IsValidState())
    {
        DebugBreak();
        return;
    }

    // Continue with defenestration.
    // Internal state is guaranteed to be good.
}

I miei commenti agli stili:

  1. Penso di preferire il secondo stile, in cui l'errore non è nascosto, a condizione che una violazione di accesso causi effettivamente un arresto anomalo.
  2. Se non è un puntatore NULL coinvolto nell'invariante, allora tendo ad inclinarmi verso il primo stile.
  3. Non mi piace molto il terzo stile, poiché nasconderà molti bug, ma conosco le persone che lo preferiscono nel codice di produzione, perché crea l'illusione di un software robusto che non si arresta in modo anomalo (le funzionalità smetteranno semplicemente di funzionare , come nella coda sull'oggetto Printer rotto).

Preferisci qualcuno di questi o hai altri modi per raggiungere questo obiettivo?

È stato utile?

Soluzione

La domanda è da considerare in combinazione con il modo in cui testate il vostro software.

È importante che colpire un invariante rotto durante il test sia archiviato come un bug ad alta gravità, proprio come sarebbe un incidente. È possibile creare build per i test durante lo sviluppo per arrestare la diagnostica morta e generare output.

Può essere appropriato aggiungere un codice difensivo, un po 'come il tuo stile 3: il tuo DebugBreak eliminerebbe la diagnostica nelle build di test, ma sarebbe solo un punto di rottura per gli sviluppatori. Ciò rende meno probabile la situazione in cui a uno sviluppatore viene impedito di funzionare da un bug in codice non correlato.

Purtroppo, l'ho visto spesso al contrario, in cui gli sviluppatori hanno tutti gli inconvenienti, ma le build di test navigano attraverso invarianti infranti. Molti bug di comportamento strano vengono archiviati, dove in realtà un singolo bug è la causa.

Altri suggerimenti

Puoi usare una tecnica chiamata NVI ( Non-Virtual-Interface ) insieme al modello metodo metodo . Probabilmente è così che lo farei (ovviamente, è solo la mia opinione personale, che è davvero discutibile):

class Printer {
public:
    // checks invariant, and calls the actual queuing
    void Queue(const PrintJob&);
private:
    virtual void DoQueue(const PringJob&);
};


void Printer::Queue(const PrintJob& job) // not virtual
{
    // Validate the state in both release and debug builds.
    // Never proceed with the queuing in a bad state.
    if(!IsValidState()) {
        throw std::logic_error("Printer not ready");
    }

    // call virtual method DoQueue which does the job
    DoQueue(job);
}

void Printer::DoQueue(const PrintJob& job) // virtual
{
    // Do the actual Queuing. State is guaranteed to be valid.
}

Poiché Queue non è virtuale, l'invariante viene comunque verificato se una classe derivata sostituisce DoQueue per una gestione speciale.


Alle tue opzioni: penso che dipenda dalla condizione che vuoi controllare.

Se si tratta di un invariante interno

  

Se è un invariante, non dovrebbe   essere possibile per un utente della tua classe   per violarlo. Alla classe dovrebbe interessare   sulla sua stessa invariante. Perciò,   vorrei asserire (CheckInvariant ()); in   un caso del genere.

È semplicemente una pre-condizione di un metodo

  

Se è semplicemente una pre-condizione che   l'utente della classe dovrebbe   garanzia (diciamo, solo dopo la stampa   la stampante è pronta), vorrei lanciare    std :: logic_error come mostrato sopra.

Scoraggerei davvero dal controllare una condizione, ma poi non fare nulla.


L'utente della classe potrebbe affermare se stesso prima di chiamare un metodo che le sue condizioni preliminari sono soddisfatte. Quindi, in generale, se una classe è responsabile di alcuni stati e trova uno stato non valido, dovrebbe affermarlo. Se la classe trova una condizione da violare che non rientra nelle sue responsabilità, dovrebbe lanciare.

È una domanda eccellente e molto pertinente. IMHO, qualsiasi architettura applicativa dovrebbe fornire una strategia per segnalare invarianti infranti. Si può decidere di utilizzare le eccezioni, di utilizzare un oggetto "Registro errori" o di verificare esplicitamente il risultato di qualsiasi azione. Forse ci sono anche altre strategie - non è questo il punto.

A seconda di un arresto possibilmente forte è una cattiva idea: non si può garantire che l'applicazione andrà in crash se non si conosce la causa della violazione invariante. In caso contrario, hai ancora dati corrotti.

La soluzione NonVirtual Interface di litb è un modo semplice per controllare gli invarianti.

Domanda difficile questa :)

Personalmente, tendo a gettare un'eccezione poiché di solito mi occupo troppo di ciò che sto facendo durante l'implementazione di cose per prendermi cura di ciò che dovrebbe essere curato dal tuo progetto. Di solito questo ritorna e mi morde più tardi ...

La mia esperienza personale con la strategia " Do-some-logging-and-then-do-nothing-more " (nessuna strategia globale, ogni classe potrebbe potenzialmente farlo in modi diversi).

Quello che avrei fatto, non appena avessi scoperto un problema del genere, sarebbe parlare con il resto del mio team e dire loro che abbiamo bisogno di una sorta di gestione globale degli errori. Ciò che la gestione farà dipende dal tuo prodotto (non vuoi semplicemente fare nulla e accedere a qualcosa in un sottile file orientato allo sviluppatore in un sistema di controller del traffico aereo, ma funzionerebbe bene se stavi realizzando un driver per, diciamo, una stampante :)).

Immagino che cosa sto dicendo, che imho, questa domanda è qualcosa che dovresti risolvere a livello di progettazione della tua applicazione piuttosto che a livello di implementazione. - E purtroppo non ci sono soluzioni magiche :(

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top