Quali sono i modi migliori per bilanciare le eccezioni informative e il codice pulito?


11

Con il nostro SDK pubblico, tendiamo a voler inviare messaggi molto informativi sul perché si verifica un'eccezione. Per esempio:

if (interfaceInstance == null)
{
     string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    throw new InvalidOperationException(errMsg);
}

Tuttavia, ciò tende a ingombrare il flusso del codice, in quanto tende a concentrarsi molto sui messaggi di errore piuttosto che su ciò che il codice sta facendo.

Un collega ha iniziato a refactoring alcune delle eccezioni lanciate a qualcosa del genere:

if (interfaceInstance == null)
    throw EmptyConstructor();

...

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

Ciò rende la logica del codice più semplice da comprendere, ma aggiunge molti altri metodi per gestire gli errori.

Quali sono altri modi per evitare il problema "logica di disordine dei messaggi di eccezione lunga"? Sto principalmente chiedendo idiomatico C # /. NET, ma anche come altre lingue lo gestiscono sono utili.

[Modificare]

Sarebbe bello avere anche i pro e i contro di ogni approccio.


4
IMHO la soluzione del tuo collega è molto buona, e immagino che se ottieni davvero molti metodi extra di quel tipo, puoi riutilizzarne almeno alcuni. Avere un sacco di piccoli metodi va bene quando i tuoi metodi sono ben nominati, purché creino blocchi del tuo programma di facile comprensione - questo sembra essere il caso qui.
Doc Brown,

@DocBrown - Sì, mi piace l'idea (aggiunta come risposta di seguito, insieme ai pro / contro), ma sia per l'intellisense che per il potenziale numero di metodi, inizia a sembrare un disordine.
FriendlyGuy

1
Pensiero: non fare del messaggio il corriere di tutti i dettagli delle eccezioni. Una combinazione di utilizzo della Exception.Dataproprietà, rilevazione dell'eccezione "esigente", chiamata della cattura del codice e aggiunta del proprio contesto, insieme allo stack di chiamate acquisite, forniscono tutte informazioni che dovrebbero consentire messaggi molto meno dettagliati. Infine System.Reflection.MethodBasesembra promettente per fornire dettagli da passare al metodo di "costruzione delle eccezioni".
radarbob,

@radarbob stai suggerendo che forse le eccezioni sono troppo dettagliate? Forse lo stiamo rendendo troppo simile alla registrazione.
FriendlyGuy

1
@MackleChan, ho letto che il paradigma qui è quello di mettere le informazioni in un messaggio che tenta di raccontare "ciò che è accaduto, è necessariamente grammaticalmente corretto, e una finzione di AI:" .. tramite il costruttore vuoto ha funzionato, ma ... " Veramente? Il mio programmatore forense interiore vede questo come una conseguenza evolutiva dell'errore comune di rilanci e perdita di conoscenza dello stack-trace-perdere Exception.Data. L'enfasi dovrebbe essere catturare la telemetria. Il refactoring qui va bene, ma manca il problema.
radarbob,

Risposte:


10

Perché non avere classi di eccezione specializzate?

if (interfaceInstance == null)
{
    throw new ThisParticularEmptyConstructorException(<maybe a couple parameters>);
}

Ciò spinge la formattazione e i dettagli all'eccezione stessa e lascia la classe principale ordinata.


1
Pro: molto pulito e organizzato, fornisce nomi significativi per ogni eccezione. Contro: potenzialmente molte classi di eccezioni extra .
FriendlyGuy

Se è importante, potresti avere un numero limitato di classi di eccezioni, passare la classe di origine dell'eccezione come parametro e avere un interruttore gigante all'interno di classi di eccezioni più generiche. Ma ha un debole odore, non sono sicuro che ci andrei.
ptyx,

che odora davvero male (eccezioni strettamente accoppiate con le classi). Immagino che sarebbe difficile bilanciare i messaggi di eccezione personalizzabili rispetto al numero di eccezioni.
FriendlyGuy

7

Microsoft sembra (guardando l'origine .NET) a volte utilizzare stringhe di risorse / ambiente. Ad esempio ParseDecimal:

throw new OverflowException(Environment.GetResourceString("Overflow_Decimal"));

Professionisti:

  • Centralizzare i messaggi delle eccezioni, consentendo il riutilizzo
  • Mantenere il messaggio di eccezione (probabilmente che non ha importanza codificare) lontano dalla logica dei metodi
  • Il tipo di eccezione generata è chiaro
  • I messaggi possono essere localizzati

Contro:

  • Se viene modificato un messaggio di eccezione, cambiano tutti
  • Il messaggio di eccezione non è così facilmente disponibile per il codice che genera l'eccezione.
  • Il messaggio è statico e non contiene informazioni sui valori errati. Se vuoi formattarlo, è più disordinato nel codice.

2
Hai tralasciato un enorme vantaggio: localizzazione del testo di eccezione
17 del 26

@ 17of26 - Buon punto, l'ho aggiunto.
FriendlyGuy

Ho votato a favore della tua risposta, ma i messaggi di errore così creati sono "statici"; non puoi aggiungere loro modificatori come l'OP sta facendo nel suo codice. Quindi hai effettivamente tralasciato alcune delle sue funzionalità.
Robert Harvey,

@RobertHarvey - L'ho aggiunto come truffatore. Questo spiega perché le eccezioni integrate non ti forniscono mai informazioni locali. Cordiali saluti, sono OP (conoscevo questa soluzione, ma volevo sapere se gli altri hanno soluzioni migliori).
FriendlyGuy

6
@ 17of26 come sviluppatore, odio le eccezioni localizzate con passione. Devo dislocarli ogni volta prima di poter, ad es. google per soluzioni.
Konrad Morawski,

2

Per lo scenario SDK pubblico, prenderei in seria considerazione l'utilizzo dei Contratti di codice Microsoft in quanto forniscono errori informativi, controlli statici e puoi anche generare documentazione da aggiungere in documenti XML e file di aiuto generati da Sandcastle . È supportato in tutte le versioni a pagamento di Visual Studio.

Un ulteriore vantaggio è che se i tuoi clienti utilizzano C #, possono sfruttare i tuoi assiemi di riferimento del contratto di codice per rilevare potenziali problemi anche prima che eseguano il loro codice.

La documentazione completa per i contratti di codice è qui .


2

La tecnica che uso è quella di combinare ed esternalizzare la convalida e lanciare del tutto una funzione di utilità.

Il singolo vantaggio più importante è che è ridotto a una linea nella logica aziendale .

Scommetto che non puoi fare di meglio se non riesci a ridurlo ulteriormente - per eliminare tutte le convalide degli argomenti e le protezioni stato-oggetto dalla logica aziendale, mantenendo solo le condizioni operative eccezionali.

Esistono, ovviamente, modi per farlo: linguaggio fortemente tipizzato, design "nessun oggetto non valido consentito in qualsiasi momento", design per contratto , ecc.

Esempio:

internal static class ValidationUtil
{
    internal static void ThrowIfRectNullOrInvalid(int imageWidth, int imageHeight, Rect rect)
    {
        if (rect == null)
        {
            throw new ArgumentNullException("rect");
        }
        if (rect.Right > imageWidth || rect.Bottom > imageHeight || MoonPhase.Now == MoonPhase.Invisible)
        {
            throw new ArgumentException(
                message: "This is uselessly informative",
                paramName: "rect");
        }
    }
}

public class Thing
{
    public void DoSomething(Rect rect)
    {
        ValidationUtil.ThrowIfRectNullOrInvalid(_imageWidth, _imageHeight, rect);
        // rest of your code
    }
}

1

[nota] Ho copiato questo dalla domanda in una risposta nel caso in cui ci siano commenti a riguardo.

Sposta ogni eccezione in un metodo della classe, prendendo in considerazione tutti gli argomenti che richiedono la formattazione.

private Exception EmptyConstructor()
{
    string errMsg = string.Format(
          "Construction of Action Argument: {0}, via the empty constructor worked, but type: {1} could not be cast to type {2}.",
          ParameterInfo.Name,
          ParameterInfo.ParameterType,
          typeof(IParameter)
    );

    return new InvalidOperationException(errMsg);
}

Racchiudere tutti i metodi di eccezione nella regione e posizionarli alla fine della classe.

Professionisti:

  • Mantiene il messaggio fuori dalla logica principale del metodo
  • Consente di aggiungere informazioni logiche a ciascun messaggio (è possibile passare argomenti al metodo)

Contro:

  • Metodo disordinato. Potenzialmente potresti avere molti metodi che restituiscono solo eccezioni e non sono realmente correlati alla logica aziendale.
  • Impossibile riutilizzare i messaggi in altre classi

Penso che i contro citati superino di gran lunga i vantaggi.
Neontapir,

@neontapir i contro sono tutti facilmente risolvibili. Ai metodi ausiliari dovrebbero essere dati IntentionRevealingNames e raggruppati in alcuni #region #endregion(che li rende nascosti dall'IDE per impostazione predefinita) e, se sono applicabili a classi diverse, inserirli in una internal static ValidationUtilityclasse. A proposito, non lamentarti mai dei nomi di identificatori lunghi davanti a un programmatore C #.
rwong

Mi lamento delle regioni però. A mio avviso, se ti ritrovi a voler ricorrere a regioni, la classe probabilmente aveva troppe responsabilità.
Neontapir,

0

Se riesci a cavartela con errori un po 'più generali, potresti scrivere una funzione di cast generica statica pubblica per te, che determina il tipo di sorgente:

public static I CastOrThrow<I,T>(T t, string source)
{
    if (t is I)
        return (I)t;

    string errMsg = string.Format(
          "Failed to complete {0}, because type: {1} could not be cast to type {2}.",
          source,
          typeof(T),
          typeof(I)
        );

    throw new InvalidOperationException(errMsg);
}


/// and then:

var interfaceInstance = SdkHelper.CastTo<IParameter>(passedObject, "Action constructor");

Ci sono variazioni possibili (si pensi SdkHelper.RequireNotNull()), che controllano solo i requisiti sugli input e che generano se falliscono, ma in questo esempio combinare il cast con la produzione del risultato è auto-documentale e compatto.

Se si utilizza .net 4.5, è possibile che il compilatore inserisca il nome del metodo / file corrente come parametro del metodo (vedere CallerMemberAttibute ). Ma per un SDK, probabilmente non puoi richiedere ai tuoi clienti di passare alla 4.5.


Si tratta più delle eccezioni che generano (e della gestione delle informazioni in un'eccezione rispetto a quanto ingombra il codice), non di questo specifico esempio di cast.
FriendlyGuy

0

Ciò che ci piace fare per gli errori di logica aziendale (non necessariamente errori di argomento ecc.) È avere un unico enum che definisca tutti i potenziali tipi di errori:

/// <summary>
/// This enum is used to identify each business rule uniquely.
/// </summary>
public enum BusinessRuleId {

    /// <summary>
    /// Indicates that a valid body weight value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyWeightMissing")]
    PatientBodyWeightMissingForDoseCalculation = 1,

    /// <summary>
    /// Indicates that a valid body height value of a patient is missing for dose calculation.
    /// </summary>
    [Display(Name = @"DoseCalculation_PatientBodyHeightMissing")]
    PatientBodyHeightMissingForDoseCalculation = 2,

    // ...
}

Gli [Display(Name = "...")]attributi definiscono la chiave nei file di risorse da utilizzare per tradurre i messaggi di errore.

Inoltre, questo file può essere utilizzato come punto di partenza per trovare tutte le occorrenze in cui un determinato tipo di errore viene generato nel codice.

Il controllo delle regole aziendali può essere delegato a classi specializzate di convalida che producono elenchi di regole aziendali violate.

Quindi utilizziamo un tipo di eccezione personalizzato per trasportare le regole violate:

[Serializable]
public class BusinessLogicException : Exception {

    /// <summary>
    /// The Business Rule that was violated.
    /// </summary>
    public BusinessRuleId ViolatedBusinessRule { get; set; }

    /// <summary>
    /// Optional: additional parameters to be used to during generation of the error message.
    /// </summary>
    public string[] MessageParameters { get; set; }

    /// <summary>
    /// This exception indicates that a Business Rule has been violated. 
    /// </summary>
    public BusinessLogicException(BusinessRuleId violatedBusinessRule, params string[] messageParameters) {
        ViolatedBusinessRule = violatedBusinessRule;
        MessageParameters = messageParameters;
    }
}

Le chiamate al servizio back-end sono racchiuse in un codice di gestione degli errori generico, che traduce la regola busines violata in un messaggio di errore leggibile dall'utente:

public object TryExecuteServiceAction(Action a) {
    try {
        return a();
    }
    catch (BusinessLogicException bex) {
        _logger.Error(GenerateErrorMessage(bex));
    }
}

public string GenerateErrorMessage(BusinessLogicException bex) {
    var translatedError = bex.ViolatedBusinessRule.ToTranslatedString();
    if (bex.MessageParameters != null) {
        translatedError = string.Format(translatedError, bex.MessageParameters);
    }
    return translatedError;
}

Ecco ToTranslatedString()un metodo di estensione enumche può leggere le chiavi delle risorse dagli [Display]attributi e usare il ResourceManagerper tradurre queste chiavi. Il valore per la rispettiva chiave di risorsa può contenere segnaposto per string.Format, che corrispondono a quelli forniti MessageParameters. Esempio di una voce nel file resx:

<data name="DoseCalculation_PatientBodyWeightMissing" xml:space="preserve">
    <value>The dose can not be calculated because the body weight observation for patient {0} is missing or not up to date.</value>
    <comment>{0} ... Patient name</comment>
</data>

Esempio di utilizzo:

throw new BusinessLogicException(BusinessRuleId.PatientBodyWeightMissingForDoseCalculation, patient.Name);

Con questo approccio, è possibile separare la generazione del messaggio di errore dalla generazione dell'errore, senza la necessità di introdurre una nuova classe di eccezione per ogni nuovo tipo di errore. Utile se frontend diversi devono visualizzare messaggi diversi, se il messaggio visualizzato dipende dalla lingua dell'utente e / o dal ruolo, ecc.


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.