Was sind gute Möglichkeiten, um informative Ausnahmen und sauberen Code in Einklang zu bringen?

11

Mit unserem öffentlichen SDK möchten wir in der Regel sehr informative Nachrichten darüber geben, warum eine Ausnahme auftritt. Beispielsweise:

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);
}

Dies führt jedoch dazu, dass der Codefluss unübersichtlich wird, da der Schwerpunkt eher auf Fehlermeldungen als auf den Aktionen des Codes liegt.

Ein Kollege hat begonnen, einige der Ausnahmen zu überarbeiten, die sich auf Folgendes auswirken:

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);
}

Dies erleichtert das Verständnis der Codelogik, fügt jedoch viele zusätzliche Methoden zur Fehlerbehandlung hinzu.

Was sind andere Möglichkeiten, um das Problem der "Clutter-Logik für Nachrichten mit langen Ausnahmen" zu vermeiden? Ich frage hauptsächlich nach idiomatischem C # /. NET, aber wie andere Sprachen damit umgehen, ist auch hilfreich.

[Bearbeiten]

Es wäre schön, auch die Vor- und Nachteile jedes Ansatzes zu haben.

FriendlyGuy
quelle
4
IMHO ist die Lösung Ihres Kollegen sehr gut, und ich denke, wenn Sie wirklich viele zusätzliche Methoden dieser Art erhalten, können Sie zumindest einige davon wiederverwenden. Viele kleine Methoden zu haben ist in Ordnung, wenn Ihre Methoden gut benannt sind, solange sie leicht verständliche Bausteine ​​Ihres Programms bilden - dies scheint hier der Fall zu sein.
Doc Brown
@DocBrown - Ja, ich mag die Idee (als Antwort unten hinzugefügt, zusammen mit Vor- / Nachteilen), aber sowohl für Intellisense als auch für die mögliche Anzahl von Methoden sieht sie auch wie Unordnung aus.
FriendlyGuy
1
Gedanke: Machen Sie die Nachricht nicht zum Träger aller Ausnahmedetails. Eine Kombination aus der Verwendung der Exception.DataEigenschaft, dem "wählerischen" Ausnahmefangen, dem Aufrufen von Code und dem Hinzufügen eines eigenen Kontexts sowie dem erfassten Aufrufstapel trägt alle Informationen bei, die weitaus weniger ausführliche Nachrichten ermöglichen sollten. Schließlich System.Reflection.MethodBasesieht es vielversprechend aus, Details bereitzustellen, die an Ihre "Ausnahmekonstruktions" -Methode übergeben werden können.
Radarbob
@radarbob schlagen Sie vor, dass die Ausnahmen vielleicht zu ausführlich sind? Vielleicht machen wir es zu sehr wie Protokollierung.
FriendlyGuy
1
@ MackleChan, ich habe gelesen, dass das Paradigma hier darin besteht, Informationen in eine Nachricht einzufügen, die versucht, genau zu sagen, was passiert ist, notwendigerweise grammatikalisch korrekt ist und einen Vorwand der KI: ".. über den leeren Konstruktor funktioniert, aber ..." "Ja wirklich?" Mein innerer forensischer Codierer sieht dies als eine evolutionäre Konsequenz des häufigen Fehlers von Stack-Trace-Verlust-Rückwürfen und Unwissenheit von Exception.Data. Der Schwerpunkt sollte auf der Erfassung der Telemetrie liegen. Refactoring hier ist in Ordnung, aber es übersieht das Problem.
Radarbob

Antworten:

10

Warum nicht spezielle Ausnahmeklassen?

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

Dadurch werden Formatierung und Details auf die Ausnahme selbst verschoben, und die Hauptklasse bleibt übersichtlich.

ptyx
quelle
1
Vorteile: Sehr sauber und organisiert, bietet aussagekräftige Namen für jede Ausnahme. Nachteile: Möglicherweise viele zusätzliche Ausnahmeklassen.
FriendlyGuy
Wenn es darauf ankommt, können Sie eine begrenzte Anzahl von Ausnahmeklassen haben, die Klasse, von der die Ausnahme stammt, als Parameter übergeben und einen riesigen Schalter in allgemeineren Ausnahmeklassen haben. Aber das hat einen schwachen Geruch - ich bin mir nicht sicher, ob ich dorthin gehen würde.
Ptyx
das riecht wirklich schlecht (eng gekoppelte Ausnahmen mit Klassen). Ich denke, es wäre schwierig, die anpassbaren Ausnahmemeldungen mit der Anzahl der Ausnahmen in Einklang zu bringen.
FriendlyGuy
7

Microsoft scheint (über die .NET-Quelle) manchmal Ressourcen- / Umgebungszeichenfolgen zu verwenden. Zum Beispiel ParseDecimal:

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

Vorteile:

  • Zentralisieren von Ausnahmemeldungen, um die Wiederverwendung zu ermöglichen
  • Halten Sie die Ausnahmemeldung (die für den Code wahrscheinlich nicht wichtig ist) von der Logik der Methoden fern
  • Die Art der ausgelösten Ausnahme ist klar
  • Nachrichten können lokalisiert werden

Nachteile:

  • Wenn eine Ausnahmemeldung geändert wird, ändern sich alle
  • Die Ausnahmemeldung ist für den Code, der die Ausnahme auslöst, nicht so leicht verfügbar.
  • Die Nachricht ist statisch und enthält keine Informationen darüber, welche Werte falsch sind. Wenn Sie es formatieren möchten, ist der Code unübersichtlicher.
FriendlyGuy
quelle
2
Sie haben einen großen Vorteil ausgelassen: Lokalisierung des Ausnahmetextes
17 vom 26.
@ 17of26 - Guter Punkt, fügte es hinzu.
FriendlyGuy
Ich habe Ihre Antwort positiv bewertet, aber auf diese Weise gemachte Fehlermeldungen sind "statisch". Sie können ihnen keine Modifikatoren hinzufügen, wie es das OP in seinem Code tut. Sie haben also einige seiner Funktionen effektiv ausgelassen.
Robert Harvey
@ RobertHarvey - Ich habe es als Betrug hinzugefügt. Das erklärt, warum integrierte Ausnahmen Ihnen nie wirklich lokale Informationen geben. Außerdem bin ich OP (ich wusste von dieser Lösung, aber ich wollte wissen, ob andere bessere Lösungen haben).
FriendlyGuy
6
@ 17of26 Als Entwickler hasse ich lokalisierte Ausnahmen mit Leidenschaft. Ich muss sie jedes Mal aufheben, bevor ich z. Google für Lösungen.
Konrad Morawski
2

Für das öffentliche SDK-Szenario würde ich dringend die Verwendung von Microsoft-Codeverträgen in Betracht ziehen, da diese informative Fehler und statische Überprüfungen enthalten und Sie auch Dokumentationen erstellen können, die in XML-Dokumente und von Sandcastle generierte Hilfedateien eingefügt werden. Es wird in allen kostenpflichtigen Versionen von Visual Studio unterstützt.

Ein zusätzlicher Vorteil besteht darin, dass Ihre Kunden, wenn sie C # verwenden, Ihre Codevertragsreferenz-Assemblys nutzen können, um potenzielle Probleme zu erkennen, noch bevor sie ihren Code ausführen.

Die vollständige Dokumentation für Codeverträge finden Sie hier .

James World
quelle
2

Die Technik, die ich verwende, besteht darin , die Validierung zu kombinieren und auszulagern und insgesamt an eine Utility-Funktion zu werfen.

Der wichtigste Vorteil ist, dass er in der Geschäftslogik auf einen Einzeiler reduziert wird .

Ich wette, Sie können es nicht besser machen, wenn Sie es nicht weiter reduzieren können - um alle Argumentvalidierungen und Objektstatuswächter aus der Geschäftslogik zu entfernen und nur die betrieblichen Ausnahmebedingungen beizubehalten.

Es gibt natürlich Möglichkeiten, dies zu tun - stark typisierte Sprache, "keine ungültigen Objekte jederzeit zulässig" Design, Design by Contract usw.

Beispiel:

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
    }
}
rwong
quelle
1

[Anmerkung] Ich habe dies aus der Frage in eine Antwort kopiert, falls es Kommentare dazu gibt.

Verschieben Sie jede Ausnahme in eine Methode der Klasse und berücksichtigen Sie dabei alle Argumente, die formatiert werden müssen.

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);
}

Schließen Sie alle Ausnahmemethoden in die Region ein und platzieren Sie sie am Ende der Klasse.

Vorteile:

  • Hält die Nachricht aus der Kernlogik der Methode heraus
  • Ermöglicht das Hinzufügen von Logikinformationen zu jeder Nachricht (Sie können Argumente an die Methode übergeben)

Nachteile:

  • Methodenunordnung. Möglicherweise gibt es viele Methoden, die nur Ausnahmen zurückgeben und nicht wirklich mit der Geschäftslogik zusammenhängen.
  • Nachrichten in anderen Klassen können nicht wiederverwendet werden
FriendlyGuy
quelle
Ich denke, die von Ihnen genannten Nachteile überwiegen bei weitem die Vorteile.
Neontapir
@neontapir die Nachteile sind alle leicht zu beheben. Hilfsmethoden sollten mit IntentionRevealingNames versehen und in einige gruppiert werden #region #endregion(wodurch sie standardmäßig vor der IDE ausgeblendet werden). Wenn sie für verschiedene Klassen gelten, sollten Sie sie einer internal static ValidationUtilityKlasse zuordnen. Übrigens, beschweren Sie sich nie über lange Bezeichnernamen vor einem C # -Programmierer.
Rwong
Ich würde mich allerdings über Regionen beschweren. Meiner Meinung nach hatte die Klasse wahrscheinlich zu viele Aufgaben, wenn Sie auf Regionen zurückgreifen möchten.
Neontapir
0

Wenn Sie mit etwas allgemeineren Fehlern davonkommen können, können Sie eine öffentliche statische generische Cast-Funktion für Sie schreiben, die den Quelltyp ableitet:

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");

Es sind Variationen möglich (denken Sie SdkHelper.RequireNotNull()), die nur die Anforderungen an die Eingaben überprüfen und werfen, wenn sie fehlschlagen. In diesem Beispiel ist die Kombination der Besetzung mit der Erstellung des Ergebnisses jedoch selbstdokumentierend und kompakt.

Wenn Sie sich in .net 4.5 befinden, kann der Compiler den Namen der aktuellen Methode / Datei als Methodenparameter einfügen (siehe CallerMemberAttibute ). Aber für ein SDK können Sie wahrscheinlich nicht verlangen, dass Ihre Kunden zu 4.5 wechseln.

jdv-Jan de Vaan
quelle
Es geht mehr um das Auslösen von Ausnahmen im Allgemeinen (und das Verwalten der Informationen in einer Ausnahme im Vergleich dazu, wie sehr sie den Code überladen), nicht um dieses spezielle Beispiel für das Casting.
FriendlyGuy
0

Was wir für Geschäftslogikfehler (nicht unbedingt Argumentationsfehler usw.) tun möchten, ist eine einzige Aufzählung, die alle möglichen Fehlertypen definiert:

/// <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,

    // ...
}

Die [Display(Name = "...")]Attribute definieren den Schlüssel in den Ressourcendateien, der zum Übersetzen der Fehlermeldungen verwendet werden soll.

Diese Datei kann auch als Ausgangspunkt verwendet werden, um alle Vorkommen zu finden, bei denen ein bestimmter Fehlertyp in Ihrem Code generiert wird.

Das Überprüfen von Geschäftsregeln kann an spezialisierte Validator-Klassen delegiert werden, die Listen mit verletzten Geschäftsregeln liefern.

Wir verwenden dann einen benutzerdefinierten Ausnahmetyp, um die verletzten Regeln zu transportieren:

[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;
    }
}

Backend-Serviceaufrufe werden in generischen Fehlerbehandlungscode eingeschlossen, der die verletzte Busines-Regel in eine vom Benutzer lesbare Fehlermeldung übersetzt:

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;
}

Hier ToTranslatedString()ist eine Erweiterungsmethode enum, mit der Ressourcenschlüssel aus [Display]Attributen gelesen und ResourceManagerzum Übersetzen dieser Schlüssel verwendet werden können. Der Wert für den jeweiligen Ressourcenschlüssel kann Platzhalter für enthalten string.Format, die mit den angegebenen übereinstimmen MessageParameters. Beispiel für einen Eintrag in der resx-Datei:

<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>

Anwendungsbeispiel:

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

Mit diesem Ansatz können Sie die Generierung der Fehlermeldung von der Generierung des Fehlers entkoppeln, ohne für jeden neuen Fehlertyp eine neue Ausnahmeklasse einführen zu müssen. Nützlich, wenn unterschiedliche Frontends unterschiedliche Nachrichten anzeigen sollen, wenn die angezeigte Nachricht von der Benutzersprache und / oder Rolle usw. abhängen soll.

Georg Patscheider
quelle