Devo registrare errori su costruttori che generano eccezioni?


15

Stavo costruendo un'applicazione da alcuni mesi e ho realizzato uno schema che è emerso:

logger.error(ERROR_MSG);
throw new Exception(ERROR_MSG);

Oppure, durante la cattura:

try { 
    // ...block that can throw something
} catch (Exception e) {
    logger.error(ERROR_MSG, e);
    throw new MyException(ERROR_MSG, e);
}

Quindi, ogni volta che lanciavo o prendevo un'eccezione, la registravo. In effetti, era quasi tutto il log che ho fatto sull'applicazione (oltre a qualcosa per l'inizializzazione dell'applicazione).

Quindi, come programmatore, evito la ripetizione; Così ho deciso di spostare le chiamate del logger nella costruzione dell'eccezione, quindi, ogni volta che stavo costruendo un'eccezione, le cose sarebbero state registrate. Potrei anche, certamente, creare un ExceptionHelper che ha generato l'eccezione per me, ma ciò renderebbe il mio codice più difficile da interpretare e, peggio ancora, il compilatore non se la sarebbe cavata bene, non realizzando che una chiamata a quel membro avrebbe lanciare immediatamente.

Quindi, questo è un anti-pattern? Se è così, perché?


E se serializzi e deserializzi l'eccezione? Registrerà l'errore?
apocalisse,

Risposte:


18

Non sono sicuro che si qualifichi come anti-pattern, ma IMO è una cattiva idea: è inutile accoppiamento per intrecciare un'eccezione con la registrazione.

Potresti non voler sempre registrare tutte le istanze di una determinata eccezione (forse si verifica durante la convalida dell'input, la cui registrazione potrebbe essere sia voluminosa che poco interessante).

In secondo luogo, potresti decidere di registrare diverse occorrenze di un errore con diversi livelli di registrazione, a quel punto dovresti specificare che durante la costruzione dell'eccezione, che rappresenta nuovamente la confusione della creazione dell'eccezione con il comportamento di registrazione.

Infine, cosa succede se si verificano eccezioni durante la registrazione di un'altra eccezione? Lo registreresti? Diventa disordinato ...

Le tue scelte sono sostanzialmente:

  • Cattura, registra e (ri) lancia, come hai dato nel tuo esempio
  • Crea una classe ExceptionHelper per fare entrambe le cose, ma gli Helpers hanno un odore di codice e non lo consiglierei neanche a me.
  • Spostare la gestione delle eccezioni generali a un livello superiore
  • Prendi in considerazione AOP per una soluzione più sofisticata a preoccupazioni trasversali come la registrazione e la gestione delle eccezioni (ma molto più complesse del semplice avere quelle due linee nei blocchi di cattura;))

+1 per "voluminoso e poco interessante". Che cos'è AOP?
Tulains Córdova,

@ user61852 Programmazione orientata all'aspetto (ho aggiunto un link). Questa questione mostra un esempio w / r / t AOP e la registrazione in Java: stackoverflow.com/questions/15746676/logging-with-aop-in-spring
Dan1701

11

Quindi, come programmatore, evito la ripetizione [...]

C'è un pericolo qui ogni volta che il concetto di "don't repeat yourself"è preso un po 'troppo sul serio fino al punto in cui diventa l'odore.


2
Ora, come diavolo dovrei scegliere la risposta corretta quando tutti sono buoni e costruire l'uno sull'altro? Ottima spiegazione di come DRY potrebbe diventare un problema se avessi un approccio fanatico ad esso.
Bruno Brant,

1
È una bella pugnalata a DRY, devo confessare che sono un DRYholic. Ora prenderò in considerazione due volte quando penso di spostare quelle 5 righe di codice da qualche altra parte per amore di DRY.
SZT

@SZaman All'inizio ero molto simile. Sul lato positivo penso che ci sia molta più speranza per quelli di noi che si sporgono troppo dal lato della riduzione della ridondanza rispetto a quelli che, diciamo, scrivono una funzione a 500 righe usando copia e incolla e non pensano nemmeno di rifattorizzarla. La cosa principale da tenere a mente l'IMO è che ogni volta che si elimina una piccola duplicazione, si sta decentralizzando il codice e reindirizzando una dipendenza altrove. Potrebbe essere una cosa positiva o negativa. Ti dà il controllo centrale per cambiare comportamento, ma la condivisione di tale comportamento potrebbe anche iniziare a morderti ...

@SZaman Se vuoi apportare una modifica del tipo, "Solo questa funzione ha bisogno di questo, gli altri che usano questa funzione centrale no." Ad ogni modo, è un atto di bilanciamento come lo vedo io - qualcosa che è difficile da fare perfettamente! Ma a volte un po 'di duplicazione può aiutare a rendere il codice più indipendente e disaccoppiato. E se provi un pezzo di codice e funziona davvero bene, anche se sta duplicando qualche logica di base qua e là, potrebbe avere qualche motivo per cambiare mai. Nel frattempo qualcosa che dipende da molte cose esterne trova molte più ragioni esterne per cambiare.

6

Per eco @ Dan1701

Si tratta di separare le preoccupazioni: spostare la registrazione nell'eccezione crea un accoppiamento stretto tra l'eccezione e la registrazione e significa anche che hai aggiunto un'ulteriore responsabilità all'eccezione della registrazione che a sua volta può creare dipendenze per la classe di eccezione che esso non serve.

Da un punto di vista della manutenzione puoi (direi) che stai nascondendo il fatto che l'eccezione viene registrata dal manutentore (almeno nel contesto di qualcosa come l'esempio), stai anche cambiando il contesto ( dalla posizione del gestore delle eccezioni al costruttore dell'eccezione) che probabilmente non è quello che intendevi.

Infine, stai assumendo che tu voglia sempre registrare un'eccezione, esattamente nello stesso modo, nel punto in cui è stato creato / generato, il che probabilmente non è il caso. A meno che non si abbiano eccezioni di registrazione e non registrazione che diventerebbero molto sgradevoli abbastanza rapidamente.

Quindi, in questo caso, penso che "SRP" vince "DRY".


1
[...]"SRP" trumps "DRY"- Penso che questa citazione praticamente la riassuma perfettamente.

Come ha detto @Ike ... Questo è il tipo di logica che stavo cercando.
Bruno Brant,

+1 per sottolineare che la modifica del contesto della registrazione verrà registrata come se la classe di eccezione fosse l'origine o la voce di registro, che non lo è.
Tulains Córdova,

2

Il tuo errore è la registrazione di un'eccezione in cui non è possibile gestirla e quindi non è possibile sapere se la registrazione fa parte della corretta gestione.
Esistono pochissimi casi in cui è possibile o è necessario gestire parte dell'errore, che potrebbe includere la registrazione, ma è comunque necessario segnalare l'errore al chiamante. Esempi sono errori di lettura non correggibili e simili, se si esegue la lettura di livello più basso, ma generalmente hanno in comune il fatto che le informazioni comunicate al chiamante sono gravemente filtrate, per sicurezza e usabilità.

L'unica cosa che puoi fare nel tuo caso, e per qualche motivo deve fare, è tradurre l'eccezione che l'implementazione getta in quella che il chiamante si aspetta, incatenando l'originale al contesto e lasciando qualcos'altro da solo.

Per riassumere, il codice si è arrogato il diritto e il dovere di gestire parzialmente l'eccezione, violando così l'SRP.
DRY non ci entra.


1

Rinviare un'eccezione solo perché hai deciso di registrarla utilizzando un blocco catch (il che significa che l'eccezione non è cambiata affatto) è una cattiva idea.

Uno dei motivi per cui utilizziamo le eccezioni, i messaggi di eccezioni e la sua gestione è che sappiamo cosa è andato storto e le eccezioni scritte in modo intelligente possono accelerare la ricerca del bug con un grande margine.

Ricorda anche che gestire le eccezioni costa molto più risorse di quante ne diciamo avere if, quindi non dovresti gestirle tutte spesso solo perché ne hai voglia. Ha un impatto sulle prestazioni della tua applicazione.

È tuttavia un buon approccio utilizzare l'eccezione come mezzo per contrassegnare il livello dell'applicazione in cui è apparso l'errore.

Considera il seguente semi-pseudo codice:

interface ICache<T, U>
{
    T GetValueByKey(U key); // may throw an CacheException
}

class FileCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from FileCache::getvalueByKey. The File could not be opened. Key: " + key);
    }
}

class RedisCache<T, U> : ICache<T, U>
{
    T GetValueByKey(U key)
    {
        throw new CacheException("Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: " + key);
    }
}

class CacheableInt
{
    ICache<int, int> cache;
    ILogger logger;

    public CacheableInt(ICache<int, int> cache, ILogger logger)
    {
        this.cache = cache;
        this.logger = logger;
    }

    public int GetNumber(int key) // may throw service exception
    {
        int result;

        try {
            result = this.cache.GetValueByKey(key);
        } catch (Exception e) {
            this.logger.Error(e);
            throw new ServiceException("CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: " + key);
        }

        return result;
    }
}

class CacheableIntService
{
    CacheableInt cacheableInt;
    ILogger logger;

    CacheableInt(CacheableInt cacheableInt, ILogger logger)
    {
        this.cacheableInt = cacheableInt;
        this.logger = logger;
    }

    int GetNumberAndReturnCode(int key)
    {
        int number;

        try {
            number = this.cacheableInt.GetNumber(key);
        } catch (Exception e) {
            this.logger.Error(e);
            return 500; // error code
        }

        return 200; // ok code
    }
}

Supponiamo che qualcuno abbia chiamato GetNumberAndReturnCodee ricevuto il 500codice, segnalando un errore. Chiamerebbe il supporto, che aprirà il file di registro e vedrebbe questo:

ERROR: 12:23:27 - Could not retrieve object from RedisCache::getvalueByKey. Failed connecting to Redis server. Redis server timed out. Key: 28
ERROR: 12:23:27 - CacheableInt::GetNumber failed, because the cache layer could not respond to request. Key: 28

Lo sviluppatore quindi conosce immediatamente quale livello del software ha causato l'interruzione del processo e ha un modo semplice per identificare il problema. In questo caso è fondamentale, perché il timeout di Redis non dovrebbe mai accadere.

Forse un altro utente chiamerebbe lo stesso metodo, ricevendo anche il 500codice, ma il registro mostrerebbe quanto segue:

INFO: 11:11:11- Could not retrieve object from RedisCache::getvalueByKey. Value does not exist for the key 28.
INFO: 11:11:11- CacheableInt::GetNumber failed, because the cache layer could not find any data for the key 28.

Nel qual caso il supporto potrebbe semplicemente rispondere all'utente che la richiesta non era valida perché sta richiedendo un valore per un ID inesistente.


Sommario

Se gestisci le eccezioni, assicurati di gestirle nel modo corretto. Assicurati anche che le tue eccezioni includano i dati / messaggi corretti, in primo luogo, seguendo i livelli dell'architettura, in modo che i messaggi ti aiuteranno a identificare un problema che può verificarsi.


1

Penso che il problema sia a un livello più elementare: si registra l'errore e lo si lancia come un'eccezione nello stesso posto. Questo è l'anti-pattern. Ciò significa che lo stesso errore viene registrato più volte se viene rilevato, forse racchiuso in un'altra eccezione e rilanciato.

Invece di questo suggerisco di registrare gli errori non quando viene creata l'eccezione, ma quando viene rilevata. (Per questo, naturalmente, è necessario fare qualche parte che sia sempre catturato.) Quando un'eccezione viene catturato, mi piacerebbe registrare solo la sua stacktrace se non ri-lanciata, oppure avvolto come causa in un'altra eccezione. Le stack stack e i messaggi delle eccezioni spostate vengono comunque registrate nelle tracce dello stack come "Causate da ...". E il ricevitore potrebbe anche decidere, ad esempio, di riprovare senza registrare l'errore al primo errore, o semplicemente trattarlo come un avvertimento o altro.


1

So che è un vecchio thread, ma ho appena riscontrato un problema simile e ho trovato una soluzione simile, quindi aggiungerò i miei 2 centesimi.

Non compro l'argomento violazione di SRP. Non del tutto comunque. Supponiamo 2 cose: 1. In realtà si desidera registrare le eccezioni quando si verificano (a livello di traccia per poter ricreare il flusso del programma). Questo non ha nulla a che fare con la gestione delle eccezioni. 2. Non puoi o non utilizzerai AOP per questo - sono d'accordo che sarebbe il modo migliore per andare ma sfortunatamente sono bloccato con un linguaggio che non fornisce strumenti per quello.

Per come la vedo io, sei sostanzialmente condannato a una violazione SRP su larga scala, perché qualsiasi classe che vuole generare eccezioni deve essere a conoscenza del registro. Lo spostamento della registrazione nella classe di eccezioni in realtà riduce notevolmente la violazione di SRP perché ora solo l'eccezione la viola e non tutte le classi nella base di codice.


0

Questo è un anti-pattern.

A mio avviso, effettuare una chiamata di registrazione nel costruttore di un'eccezione sarebbe un esempio di quanto segue: difetto: il costruttore funziona davvero .

Non mi aspetterei mai (o desidererei) che un costruttore effettuasse una chiamata di servizio esterna. Questo è un effetto collaterale molto indesiderabile che, come sottolinea Miško Hevery, forza le sottoclassi e le derisioni a ereditare comportamenti indesiderati.

Come tale, violerebbe anche il principio del minimo stupore .

Se stai sviluppando un'applicazione con altri, probabilmente questo effetto collaterale non sarà evidente a loro. Anche se lavori da solo, potresti dimenticartene e sorprenderti lungo la strada.

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.