it-swarm.com.de

Ein Flag, das angibt, ob wir Fehler auslösen sollen

Ich habe kürzlich angefangen, an einem Ort mit einigen viel älteren Entwicklern (über 50 Jahre alt) zu arbeiten. Sie haben an kritischen Anwendungen für die Luftfahrt gearbeitet, bei denen das System nicht ausfallen konnte. Infolgedessen tendiert der ältere Programmierer dazu, auf diese Weise zu codieren.

Er neigt dazu, einen Booleschen Wert in die Objekte einzufügen, um anzugeben, ob eine Ausnahme ausgelöst werden soll oder nicht.

Beispiel

public class AreaCalculator
{
    AreaCalculator(bool shouldThrowExceptions) { ... }
    CalculateArea(int x, int y)
    {
        if(x < 0 || y < 0)
        {
            if(shouldThrowExceptions) 
                throwException;
            else
                return 0;
        }
    }
}

(In unserem Projekt kann die Methode fehlschlagen, da wir versuchen, ein Netzwerkgerät zu verwenden, das zu diesem Zeitpunkt nicht vorhanden sein kann. Das Bereichsbeispiel ist nur ein Beispiel für das Ausnahmeflag.)

Für mich scheint das ein Code-Geruch zu sein. Das Schreiben von Komponententests wird etwas komplexer, da Sie jedes Mal auf das Ausnahmeflag testen müssen. Wenn etwas schief geht, möchten Sie es dann nicht sofort wissen? Sollte es nicht in der Verantwortung des Anrufers liegen, zu bestimmen, wie er fortfahren soll?

Seine Logik/Argumentation ist, dass unser Programm eine Sache tun muss, um dem Benutzer Daten zu zeigen. Jede andere Ausnahme, die uns nicht davon abhält, sollte ignoriert werden. Ich bin damit einverstanden, dass sie nicht ignoriert werden sollten, sondern in die Luft sprudeln und von der entsprechenden Person behandelt werden sollten und sich dafür nicht mit Flaggen befassen müssen.

Ist dies eine gute Möglichkeit, mit Ausnahmen umzugehen?

Bearbeiten: Nur um mehr Kontext über die Entwurfsentscheidung zu geben, vermute ich, dass das Programm, wenn diese Komponente ausfällt, weiterhin arbeiten und seine Hauptaufgabe ausführen kann. Daher möchten wir keine Ausnahme auslösen (und nicht damit umgehen?) Und das Programm herunterfahren lassen, wenn es für den Benutzer einwandfrei funktioniert

Edit 2: Um noch mehr Kontext zu erhalten, wird in unserem Fall die Methode aufgerufen, um eine Netzwerkkarte zurückzusetzen. Das Problem tritt auf, wenn die Netzwerkkarte getrennt und erneut verbunden wird und ihr eine andere IP-Adresse zugewiesen wird. Beim Zurücksetzen wird daher eine Ausnahme ausgelöst, da versucht wird, die Hardware mit der alten IP zurückzusetzen.

64
Nicolas

Das Problem bei diesem Ansatz besteht darin, dass Ausnahmen zwar nie ausgelöst werden (und die Anwendung daher niemals aufgrund nicht erfasster Ausnahmen abstürzt), die zurückgegebenen Ergebnisse jedoch nicht unbedingt korrekt sind und der Benutzer möglicherweise nie weiß, dass ein Problem mit den Daten vorliegt (oder Was ist das für ein Problem und wie kann man es beheben?.

Damit die Ergebnisse korrekt und aussagekräftig sind, muss die aufrufende Methode das Ergebnis auf spezielle Zahlen prüfen - d. H. Auf bestimmte Rückgabewerte, die zur Kennzeichnung von Problemen verwendet werden, die während der Ausführung der Methode aufgetreten sind. Negative (oder Null-) Zahlen, die für positiv definierte Mengen (wie Fläche) zurückgegeben werden, sind ein Paradebeispiel dafür in älterem Code. Wenn die aufrufende Methode nicht weiß (oder vergisst!), Nach diesen speziellen Nummern zu suchen, kann die Verarbeitung fortgesetzt werden, ohne jemals einen Fehler zu bemerken. Dem Benutzer werden dann Daten angezeigt, die einen Bereich von 0 anzeigen, von dem der Benutzer weiß, dass er falsch ist, aber er hat keinen Hinweis darauf, was wo, warum oder warum schief gelaufen ist. Sie fragen sich dann, ob einer der anderen Werte falsch ist ...

Wenn die Ausnahme ausgelöst würde, würde die Verarbeitung gestoppt, der Fehler würde (idealerweise) protokolliert und der Benutzer könnte auf irgendeine Weise benachrichtigt werden. Der Benutzer kann dann das Problem beheben und es erneut versuchen. Durch eine ordnungsgemäße Ausnahmebehandlung (und Tests!) Wird sichergestellt, dass kritische Anwendungen nicht abstürzen oder auf andere Weise in einem ungültigen Zustand enden.

73
mmathis

Ist dies eine gute Möglichkeit, mit Ausnahmen umzugehen?

Nein, ich denke das ist eine ziemlich schlechte Praxis. Das Auslösen einer Ausnahme im Vergleich zum Zurückgeben eines Werts ist eine grundlegende Änderung in der API, das Ändern der Signatur der Methode und das Verhalten der Methode aus Sicht der Benutzeroberfläche ganz anders.

Wenn wir Klassen und ihre APIs entwerfen, sollten wir dies im Allgemeinen berücksichtigen

  1. es können mehrere Instanzen der Klasse mit unterschiedlichen Konfigurationen gleichzeitig im selben Programm vorhanden sein.

  2. aufgrund der Abhängigkeitsinjektion und einer beliebigen Anzahl anderer Programmierpraktiken kann ein konsumierender Client die Objekte erstellen und an einen anderen übergeben, um sie zu verwenden. Daher besteht häufig eine Trennung zwischen Objekterstellern und Objektbenutzern.

Überlegen Sie nun, was der Methodenaufrufer tun muss, um eine Instanz zu verwenden, die ihm übergeben wurde, z. für den Aufruf der Berechnungsmethode: Der Anrufer müsste sowohl prüfen, ob der Bereich Null ist, als auch Ausnahmen abfangen - autsch! Überlegungen zum Testen beziehen sich nicht nur auf die Klasse selbst, sondern auch auf die Fehlerbehandlung der Anrufer ...

Wir sollten es dem konsumierenden Kunden immer so einfach wie möglich machen. Diese boolesche Konfiguration im Konstruktor, die die API einer Instanzmethode ändert, ist das Gegenteil davon, dass der konsumierende Client-Programmierer (möglicherweise Sie oder Ihr Kollege) in die Erfolgsgrube fällt.

Um beide APIs anzubieten, ist es viel besser und normaler, entweder zwei verschiedene Klassen bereitzustellen - eine, die immer Fehler auslöst, und eine, die bei Fehlern immer 0 zurückgibt, oder zwei verschiedene Methoden mit einer einzigen Klasse bereitzustellen. Auf diese Weise kann der konsumierende Client leicht genau wissen, wie er nach Fehlern sucht und diese behandelt.

Mit zwei verschiedenen Klassen oder zwei verschiedenen Methoden können Sie die IDEs verwenden, um Methodenbenutzer und Refactor-Funktionen usw. zu finden. Dies ist viel einfacher, da die beiden Anwendungsfälle nicht mehr zusammengeführt werden. Das Lesen, Schreiben, Warten, Überprüfen und Testen von Code ist ebenfalls einfacher.


In einem anderen Punkt bin ich persönlich der Meinung, dass wir keine booleschen Konfigurationsparameter verwenden sollten, wobei die tatsächlichen Aufrufer alle einfach eine Konstante übergeben. Eine solche Konfigurationsparametrisierung kombiniert zwei separate Anwendungsfälle ohne wirklichen Nutzen.

Sehen Sie sich Ihre Codebasis an und prüfen Sie, ob jemals eine Variable (oder ein nicht konstanter Ausdruck) für den booleschen Konfigurationsparameter im Konstruktor verwendet wird! Ich bezweifle das.


Eine weitere Überlegung ist die Frage, warum die Berechnung des Bereichs fehlschlagen kann. Am besten werfen Sie den Konstruktor ein, wenn die Berechnung nicht möglich ist. Wenn Sie jedoch nicht wissen, ob die Berechnung durchgeführt werden kann, bis das Objekt weiter initialisiert ist, sollten Sie möglicherweise verschiedene Klassen verwenden, um diese Zustände zu unterscheiden (nicht bereit, Bereich zu berechnen, oder bereit, Bereich zu berechnen).

Ich habe gelesen, dass Ihre Fehlersituation auf Remoting ausgerichtet ist und daher möglicherweise nicht zutrifft. nur ein paar Denkanstöße.


Sollte es nicht in der Verantwortung des Anrufers liegen, zu bestimmen, wie er fortfahren soll?

Ja, ich stimme zu. Es erscheint für den Angerufenen verfrüht, zu entscheiden, dass der Bereich 0 unter Fehlerbedingungen die richtige Antwort ist (insbesondere, da 0 ein gültiger Bereich ist und daher keine Möglichkeit besteht, den Unterschied zwischen Fehler und tatsächlicher 0 zu erkennen, obwohl dies möglicherweise nicht für Ihre App gilt).

47
Erik Eidt

Sie haben an kritischen Anwendungen für die Luftfahrt gearbeitet, bei denen das System nicht ausfallen konnte. Als Ergebnis ...

Das ist eine interessante Einführung, die mir den Eindruck vermittelt, dass die Motivation hinter diesem Entwurf darin besteht, in einigen Kontexten keine Ausnahmen auszulösen , "weil das System ausfallen könnte" dann. Wenn das System jedoch "aufgrund einer Ausnahme ausfallen kann", ist dies ein klarer Hinweis darauf

  • Ausnahmen werden nicht richtig behandelt, zumindest noch starr.

Wenn also das Programm, das AreaCalculator verwendet, fehlerhaft ist, zieht es Ihr Kollege vor, das Programm nicht "früh abstürzen" zu lassen, sondern einen falschen Wert zurückzugeben (in der Hoffnung, dass niemand es bemerkt oder dass niemand etwas Wichtiges damit macht ). Das ist eigentlich Maskierung eines Fehlers, und meiner Erfahrung nach wird es früher oder später zu Folgefehlern führen, für die es schwierig wird, die Grundursache zu finden.

IMHO ist das Schreiben eines Programms, das unter keinen Umständen abstürzt, aber falsche Daten oder Berechnungsergebnisse anzeigt, normalerweise keineswegs besser, als das Programm abstürzen zu lassen. Der einzig richtige Ansatz besteht darin, dem Anrufer die Möglichkeit zu geben, den Fehler zu bemerken, ihn zu behandeln, ihn entscheiden zu lassen, ob der Benutzer über das falsche Verhalten informiert werden muss und ob es sicher ist, die Verarbeitung fortzusetzen, oder ob es sicherer ist um das Programm vollständig zu stoppen. Daher würde ich eines der folgenden empfehlen:

  • machen Sie es schwer zu übersehen, dass eine Funktion eine Ausnahme auslösen kann. Dokumentations- und Codierungsstandards sind hier Ihr Freund, und regelmäßige Codeüberprüfungen sollten die korrekte Verwendung von Komponenten und die ordnungsgemäße Behandlung von Ausnahmen unterstützen.

  • trainieren Sie das Team darin, Ausnahmen zu erwarten und zu behandeln, wenn es "Black Box" -Komponenten verwendet, und berücksichtigen Sie das globale Verhalten des Programms.

  • wenn Sie aus bestimmten Gründen der Meinung sind, dass Sie den aufrufenden Code (oder die Entwickler, die ihn schreiben) nicht dazu bringen können, die Ausnahmebehandlung ordnungsgemäß zu verwenden, können Sie als letzten Ausweg eine API mit expliziten Fehlerausgabevariablen und überhaupt keinen Ausnahmen entwerfen

    CalculateArea(int x, int y, out ErrorCode err)
    

    daher wird es für den Anrufer sehr schwierig, die Funktion zu übersehen, die fehlschlagen könnte. Aber das ist meiner Meinung nach extrem hässlich in C #; Es ist eine alte defensive Programmiertechnik von C, bei der es keine Ausnahmen gibt, und es sollte heutzutage normalerweise nicht notwendig sein, so zu arbeiten.

37
Doc Brown

Das Schreiben von Komponententests wird etwas komplexer, da Sie jedes Mal auf das Ausnahmeflag testen müssen.

Jede Funktion mit n Parametern ist schwieriger zu testen als eine mit n-1 Parametern. Erweitern Sie das auf das Absurde und das Argument wird, dass Funktionen überhaupt keine Parameter haben sollten, da sie dadurch am einfachsten zu testen sind.

Während es eine großartige Idee ist, Code zu schreiben, der einfach zu testen ist, ist es eine schreckliche Idee, die Einfachheit des Tests über das Schreiben von Code zu stellen, der für die Leute nützlich ist, die ihn aufrufen müssen. Wenn das Beispiel in der Frage einen Schalter enthält, der bestimmt, ob eine Ausnahme ausgelöst wird oder nicht, ist es möglich, dass die Nummernaufrufer, die dieses Verhalten wünschen, es verdient haben, es der Funktion hinzuzufügen. Wo die Grenze zwischen komplex und zu komplex liegt, ist ein Urteilsspruch; Jeder, der versucht, Ihnen zu sagen, dass es eine helle Linie gibt, die in allen Situationen gilt, sollte mit Argwohn betrachtet werden.

Wenn etwas schief geht, möchten Sie es dann nicht sofort wissen?

Das hängt von Ihrer Definition von falsch ab. Das Beispiel in der Frage definiert falsch als "bei einer Dimension von weniger als Null und shouldThrowExceptions ist wahr". Eine Dimension zu erhalten, wenn weniger als Null nicht falsch ist, wenn shouldThrowExceptions falsch ist, weil der Schalter ein anderes Verhalten hervorruft. Das ist ganz einfach keine Ausnahmesituation.

Das eigentliche Problem hierbei ist, dass der Switch schlecht benannt wurde, da er nicht beschreibt, was die Funktion bewirkt. Hätte man ihm einen besseren Namen wie treatInvalidDimensionsAsZero gegeben, hätten Sie diese Frage gestellt?

Sollte es nicht in der Verantwortung des Anrufers liegen, zu bestimmen, wie er fortfahren soll?

Der Anrufer tut bestimmt, wie er fortfahren soll. In diesem Fall erfolgt dies vorab durch Setzen oder Löschen von shouldThrowExceptions, und die Funktion verhält sich entsprechend ihrem Status.

Das Beispiel ist pathologisch einfach, da es eine einzelne Berechnung durchführt und zurückgibt. Wenn Sie es etwas komplexer gestalten, z. B. die Summe der Quadratwurzeln einer Zahlenliste berechnen, kann das Auslösen von Ausnahmen Anrufern Probleme bereiten, die sie nicht lösen können. Wenn ich eine Liste von [5, 6, -1, 8, 12] und die Funktion löst eine Ausnahme über -1, Ich kann der Funktion nicht sagen, dass sie weitermachen soll, da sie die Summe bereits abgebrochen und weggeworfen hat. Wenn es sich bei der Liste um einen riesigen Datensatz handelt, kann es unpraktisch sein, vor dem Aufrufen der Funktion eine Kopie ohne negative Zahlen zu erstellen. Daher muss ich im Voraus sagen, wie ungültige Zahlen behandelt werden sollen, entweder in Form eines "Ignorieren Sie sie einfach" "wechseln oder vielleicht ein Lambda bereitstellen, das gerufen wird, um diese Entscheidung zu treffen.

Seine Logik/Argumentation ist, dass unser Programm eine Sache tun muss, um dem Benutzer Daten zu zeigen. Jede andere Ausnahme, die uns nicht davon abhält, sollte ignoriert werden. Ich bin damit einverstanden, dass sie nicht ignoriert werden sollten, sondern in die Luft sprudeln und von der entsprechenden Person behandelt werden sollten und sich dafür nicht mit Flaggen befassen müssen.

Auch hier gibt es keine Einheitslösung. Im Beispiel wurde die Funktion vermutlich in eine Spezifikation geschrieben, die angibt, wie mit negativen Dimensionen umgegangen werden soll. Das Letzte, was Sie tun möchten, ist, das Signal-Rausch-Verhältnis Ihrer Protokolle zu verringern, indem Sie sie mit Nachrichten füllen, die besagen: "Normalerweise wird hier eine Ausnahme ausgelöst, aber der Anrufer hat gesagt, dass er sich nicht darum kümmern soll."

Und als einer dieser viel älteren Programmierer würde ich Sie bitten, sich freundlicherweise von meinem Rasen zu entfernen. ;-);

13
Blrfl

Sicherheitskritischer und "normaler" Code kann zu sehr unterschiedlichen Vorstellungen darüber führen, wie "bewährte Verfahren" aussehen. Es gibt viele Überschneidungen - einige Dinge sind riskant und sollten in beiden Fällen vermieden werden -, aber es gibt immer noch signifikante Unterschiede. Wenn Sie eine Anforderung hinzufügen, die garantiert reagiert, werden diese Abweichungen erheblich.

Diese beziehen sich oft auf Dinge, die Sie erwarten würden:

  • Für git kann die falsche Antwort sehr schlecht sein in Bezug auf: zu lange dauern/abbrechen/hängen oder sogar abstürzen (was praktisch kein Problem ist, wenn beispielsweise der eingecheckte Code versehentlich geändert wird).

    Allerdings: Bei einer Instrumententafel mit einer g-Kraft-Berechnung, die blockiert und verhindert, dass eine Luftgeschwindigkeitsberechnung durchgeführt wird, ist dies möglicherweise nicht akzeptabel.

Einige sind weniger offensichtlich:

  • Wenn Sie ein Los getestet haben, sind Ergebnisse erster Ordnung (wie richtige Antworten) relativ gesehen keine so große Sorge. Sie wissen, dass Ihre Tests dies abgedeckt haben. Wenn es jedoch einen verborgenen Zustand oder Kontrollfluss gab, wissen Sie nicht, dass dies nicht die Ursache für etwas viel subtileres ist. Dies ist beim Testen schwer auszuschließen.

  • Nachweislich sicher zu sein ist relativ wichtig. Nicht viele Kunden werden sich hinsetzen, um zu überlegen, ob die Quelle, die sie kaufen, sicher ist oder nicht. Wenn Sie andererseits auf dem Luftfahrtmarkt sind ...

Wie trifft dies auf Ihr Beispiel zu:

Ich weiß es nicht. Es gibt eine Reihe von Denkprozessen, bei denen möglicherweise Leitregeln wie "Niemand wirft Produktionscode ein" in sicherheitskritischen Code übernommen werden, die in normalen Situationen ziemlich albern wären.

Einige beziehen sich auf das Einbetten, einige auf die Sicherheit und vielleicht andere ... Einige sind gut (enge Leistungs-/Speichergrenzen waren erforderlich), andere sind schlecht (wir behandeln Ausnahmen nicht richtig, also riskieren Sie es am besten nicht). Die meiste Zeit wird die Frage nicht wirklich beantwortet, selbst wenn man weiß, warum sie es getan haben. Wenn es zum Beispiel darum geht, den Code einfacher zu prüfen, als ihn tatsächlich zu verbessern, ist es dann eine gute Praxis? Das kann man wirklich nicht sagen. Sie sind verschiedene Tiere und müssen unterschiedlich behandelt werden.

Alles in allem sieht es für mich ein bisschen verdächtig aus [~ # ~] aber [~ # ~] :

Sicherheitskritische Software- und Software-Design-Entscheidungen sollten wahrscheinlich nicht von Fremden beim Software-Engineering-Stack-Austausch getroffen werden. Es kann durchaus einen guten Grund geben, dies zu tun, auch wenn es Teil eines schlechten Systems ist. Lesen Sie nicht zu viel darüber, außer als "Denkanstoß".

8
drjpizzle

Manchmal ist das Auslösen einer Ausnahme nicht die beste Methode. Nicht zuletzt aufgrund des Abwickelns des Stapels, sondern manchmal auch, weil das Abfangen einer Ausnahme problematisch ist, insbesondere entlang von Sprach- oder Schnittstellennähten.

Eine gute Möglichkeit, dies zu handhaben, besteht darin, einen angereicherten Datentyp zurückzugeben. Dieser Datentyp hat genug Status, um alle glücklichen Pfade und alle unglücklichen Pfade zu beschreiben. Der Punkt ist, wenn Sie mit dieser Funktion interagieren (Mitglied/global/sonst), werden Sie gezwungen sein, das Ergebnis zu behandeln.

Davon abgesehen sollte dieser angereicherte Datentyp keine Aktion erzwingen. Stellen Sie sich in Ihrem Beispiel etwas wie var area_calc = new AreaCalculator(); var volume = area_calc.CalculateArea(x, y) * z; vor. Scheint nützlich volume sollte die Fläche multipliziert mit der Tiefe enthalten - das kann ein Würfel, ein Zylinder usw. sein.

Aber was ist, wenn der Dienst area_calc nicht verfügbar ist? Dann gab area_calc .CalculateArea(x, y) einen umfangreichen Datentyp zurück, der einen Fehler enthielt. Ist es legal, das mit z zu multiplizieren? Das ist eine gute Frage. Sie können Benutzer zwingen, die Überprüfung sofort durchzuführen. Dies bricht jedoch die Logik bei der Fehlerbehandlung auf.

var area_calc = new AreaCalculator();
var area_result = area_calc.CalculateArea(x, y);
if (area_result.bad())
{
    //handle unhappy path
}
var volume = area_result.value() * z;

vs.

var area_calc = new AreaCalculator();
var volume = area_calc.CalculateArea(x, y) * z;
if (volume.bad())
{
    //handle unhappy path
}

Die im Wesentlichen logische Verteilung ist auf zwei Zeilen verteilt und im ersten Fall durch die Fehlerbehandlung unterteilt, während im zweiten Fall die gesamte relevante Logik in einer Zeile enthalten ist, gefolgt von der Fehlerbehandlung.

In diesem zweiten Fall ist volume ein umfangreicher Datentyp. Es ist nicht nur eine Zahl. Dies vergrößert den Speicher und volume muss noch auf einen Fehlerzustand untersucht werden. Zusätzlich kann volume andere Berechnungen unterstützen, bevor der Benutzer den Fehler behandelt, sodass er sich an mehreren unterschiedlichen Stellen manifestieren kann. Dies kann je nach den Besonderheiten der Situation gut oder schlecht sein.

Alternativ könnte volume nur ein einfacher Datentyp sein - nur eine Zahl, aber was passiert dann mit der Fehlerbedingung? Es kann sein, dass der Wert implizit konvertiert wird, wenn er sich in einem glücklichen Zustand befindet. Sollte es sich in einem unglücklichen Zustand befinden, wird möglicherweise ein Standard-/Fehlerwert zurückgegeben (für Bereich 0 oder -1 erscheint dies möglicherweise sinnvoll). Alternativ könnte eine Ausnahme auf dieser Seite der Schnittstelle/Sprachgrenze ausgelöst werden.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}
var volume = foo();
if (volume <= 0)
{
    //handle error
}

vs.

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}

try { var volume = foo(); }
catch(...)
{
    //handle error
}

Durch das Verteilen eines schlechten oder möglicherweise schlechten Werts muss der Benutzer die Daten validieren. Dies ist eine Fehlerquelle, da der Rückgabewert für den Compiler eine legitime Ganzzahl ist. Wenn etwas nicht überprüft wurde, werden Sie es entdecken, wenn etwas schief geht. Der zweite Fall mischt das Beste aus beiden Welten, indem Ausnahmen unglückliche Pfade verarbeiten können, während glückliche Pfade der normalen Verarbeitung folgen. Leider zwingt es den Benutzer, Ausnahmen mit Bedacht zu behandeln, was schwierig ist.

Nur um klar zu sein, dass ein unglücklicher Pfad ein Fall ist, der der Geschäftslogik (der Domäne der Ausnahme) unbekannt ist. Die Nichtvalidierung ist ein glücklicher Pfad, da Sie wissen, wie Sie mit den Geschäftsregeln (der Domäne der Regeln) umgehen.

Die ultimative Lösung wäre eine, die alle Szenarien (innerhalb des vernünftigen Rahmens) zulässt.

  • Der Benutzer sollte in der Lage sein, nach einem schlechten Zustand zu fragen und diesen sofort zu behandeln
  • Der Benutzer sollte in der Lage sein, den angereicherten Typ so zu bearbeiten, als ob der glückliche Pfad befolgt worden wäre, und die Fehlerdetails weiterzugeben.
  • Der Benutzer sollte in der Lage sein, den Wert für den glücklichen Pfad durch Casting (implizit/explizit, wie es sinnvoll ist) zu extrahieren, wodurch eine Ausnahme für unglückliche Pfade generiert wird.
  • Der Benutzer sollte in der Lage sein, den Wert für den glücklichen Pfad zu extrahieren oder einen Standardwert (angegeben oder nicht) zu verwenden.

Etwas wie:

Rich::value_type value_or_default(Rich&, Rich::value_type default_value = ...);
bool bad(Rich&);
...unhappy path report... bad_state(Rich&);
Rich& assert_not_bad(Rich&);
class Rich
{
public:
   typedef ... value_type;

   operator value_type() { assert_not_bad(*this); return ...value...; }
   operator X(...) { if (bad(*this)) return ...propagate badness to new value...; /*operate and generate new value*/; }
}

//check
if (bad(x))
{
    var report = bad_state(x);
    //handle error
}

//rethrow
assert_not_bad(x);
var result = (assert_not_bad(x) + 23) / 45;

//propogate
var y = x * 23;

//implicit throw
Rich::value_type val = x;
var val = ((Rich::value_type)x) + 34;
var val2 = static_cast<Rich::value_type>(x) % 3;

//default value
var defaulted = value_or_default(x);
var defaulted_to = value_or_default(x, 55);
7
Kain0_0

Ich werde aus Sicht von C++ antworten. Ich bin mir ziemlich sicher, dass alle Kernkonzepte auf C # übertragbar sind.

Es hört sich so an, als ob Ihr bevorzugter Stil "immer Ausnahmen werfen" ist:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

Dies kann ein Problem für C++ - Code sein, da die Ausnahmebehandlung schwer ist . Dadurch wird der Fehlerfall langsam ausgeführt und der Fehlerfall reserviert Speicher (der manchmal nicht einmal verfügbar) und macht die Dinge im Allgemeinen weniger vorhersehbar. Das Schwergewicht von EH ist einer der Gründe, warum Sie Leute sagen hören: "Verwenden Sie keine Ausnahmen für den Kontrollfluss."

Also einige Bibliotheken (wie <filesystem> ) Verwenden Sie, was C++ eine "duale API" nennt, oder was C # das Try-Parse pattern (danke Peter für den Tipp!)

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

bool TryCalculateArea(int x, int y, int& result) {
    if (x < 0 || y < 0) {
        return false;
    }
    result = x * y;
    return true;
}

int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
    // use a2
}

Sie können das Problem mit "dualen APIs" sofort erkennen: viele Codeduplizierungen, keine Anleitung für Benutzer, welche API die "richtige" ist, und der Benutzer muss eine schwierige Wahl zwischen nützliche Fehlermeldungen (CalculateArea) und Geschwindigkeit (TryCalculateArea ) weil die schnellere Version unser nützliches "negative side lengths" Ausnahme und glättet es in eine nutzlose false - "etwas ist schief gelaufen, frag mich nicht was oder wo." (Einige duale APIs verwenden einen aussagekräftigeren Fehlertyp, z. B. int errno oder C++ 's std::error_code, aber das sagt dir immer noch nicht wo der Fehler aufgetreten ist - nur dass es getan hat irgendwo auftreten.)

Wenn Sie sich nicht entscheiden können, wie sich Ihr Code verhalten soll, können Sie die Entscheidung jederzeit dem Anrufer überlassen!

template<class F>
int CalculateArea(int x, int y, F errorCallback) {
    if (x < 0 || y < 0) {
        return errorCallback(x, y, "negative side lengths");
    }
    return x * y;
}

int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });

Dies ist im Wesentlichen das, was Ihr Mitarbeiter tut. außer dass er den "Fehlerhandler" in eine globale Variable zerlegt:

std::function<int(const char *)> g_errorCallback;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorCallback("negative side lengths");
    }
    return x * y;
}

g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);

Das Verschieben wichtiger Parameter von expliziten Funktionsparametern in den globalen Zustand ist fast immer a schlechte Idee. Ich empfehle es nicht. (Die Tatsache, dass es in Ihrem Fall kein globaler Status ist, sondern einfach instanzweit Mitgliedstaat mildert die Schlechtigkeit ein wenig, aber nicht viel.)

Darüber hinaus begrenzt Ihr Mitarbeiter unnötig die Anzahl möglicher Fehlerbehandlungsverhalten. Anstatt ein Lambda zur Fehlerbehandlung zuzulassen, hat er sich für nur zwei entschieden:

bool g_errorViaException;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorViaException ? throw Exception("negative side lengths") : 0;
    }
    return x * y;
}

g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);

Dies ist wahrscheinlich der "saure Punkt" einer dieser möglichen Strategien. Sie haben dem Endbenutzer die gesamte Flexibilität genommen, indem Sie ihn gezwungen haben, einen Ihrer genau zwei Rückrufe zur Fehlerbehandlung zu verwenden. und Sie haben alle Probleme des gemeinsamen globalen Zustands; und Sie zahlen immer noch überall für diesen bedingten Zweig.

Schließlich besteht eine gängige Lösung in C++ (oder einer beliebigen Sprache mit bedingter Kompilierung) darin, den Benutzer zu zwingen, die Entscheidung für sein gesamtes Programm global zur Kompilierungszeit zu treffen, damit der nicht verwendete Codepfad vollständig optimiert werden kann:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
        return 0;
#else
        throw Exception("negative side lengths");
#endif
    }
    return x * y;
}

// Now these two function calls *must* have the same behavior,
// which is a Nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);

Ein Beispiel für etwas, das auf diese Weise funktioniert, ist das assert Makro in C und C++, das sein Verhalten auf dem Präprozessormakro NDEBUG bestimmt.

3
Quuxplusone

Ich denke, es sollte erwähnt werden, woher Ihr Kollege sein Muster hat.

Heutzutage hat C # das TryGet-Muster public bool TryThing(out result). Auf diese Weise können Sie Ihr Ergebnis abrufen und gleichzeitig wissen, ob dieses Ergebnis überhaupt ein gültiger Wert ist. (Zum Beispiel sind alle int -Werte gültige Ergebnisse für Math.sum(int, int), aber wenn der Wert überläuft, könnte dieses bestimmte Ergebnis Müll sein). Dies ist jedoch ein relativ neues Muster.

Vor dem Schlüsselwort out mussten Sie entweder eine Ausnahme auslösen (teuer, und der Aufrufer muss sie abfangen oder das gesamte Programm beenden), eine spezielle Struktur erstellen (Klasse vor Klasse oder Generika waren wirklich eine Sache) für Jedes Ergebnis repräsentiert den Wert und mögliche Fehler (zeitaufwändig, um Software zu erstellen und aufzublähen) oder gibt einen Standardwert für "Fehler" zurück (der möglicherweise kein Fehler war).

Der Ansatz, den Ihr Kollege verwendet, gibt ihm den Vorteil, dass beim Testen/Debuggen neuer Funktionen frühzeitig Ausnahmen fehlgeschlagen sind, und bietet ihm gleichzeitig die Laufzeitsicherheit und -leistung (Leistung war vor ca. 30 Jahren ein kritisches Problem), nur einen Standardfehlerwert zurückzugeben. Dies ist das Muster, in das die Software geschrieben wurde, und das erwartete Muster, das sich weiterentwickelt. Es ist daher selbstverständlich, dies auch weiterhin so zu tun, obwohl es jetzt bessere Möglichkeiten gibt. Höchstwahrscheinlich wurde dieses Muster aus dem Zeitalter der Software übernommen oder aus einem Muster, aus dem Ihre Colleges einfach nie herausgewachsen sind (alte Gewohnheiten sind schwer zu brechen).

In den anderen Antworten wird bereits erläutert, warum dies als schlechte Vorgehensweise angesehen wird. Daher empfehle ich Ihnen abschließend, sich über das TryGet-Muster zu informieren (möglicherweise auch die Kapselung dessen, was ein Objekt seinem Aufrufer versprechen sollte).

1
Tezra

Dieses spezielle Beispiel hat eine interessante Funktion, die sich auf die Regeln auswirken kann ...

CalculateArea(int x, int y)
{
    if(x < 0 || y < 0)
    {
        if(shouldThrowExceptions) 
            throwException;
        else
            return 0;
    }
}

Was ich hier sehe, ist ein Vorbedingung Check. Eine fehlgeschlagene Voraussetzungsprüfung impliziert einen Fehler weiter oben im Aufrufstapel. Daher stellt sich die Frage, ob dieser Code für die Meldung von Fehlern an anderer Stelle verantwortlich ist.

Ein Teil der Spannung hier ist auf die Tatsache zurückzuführen, dass diese Schnittstelle primitive Besessenheit - x und y aufweist, von denen angenommen wird, dass sie reale Längenmaße darstellen. In einem Programmierkontext, in dem domänenspezifische Typen eine vernünftige Wahl sind, würden wir die Vorbedingungsprüfung tatsächlich näher an die Datenquelle rücken - mit anderen Worten, die Verantwortung für die Datenintegrität weiter oben im Aufrufstapel, wo wir eine haben besserer Sinn für den Kontext.

Trotzdem sehe ich nichts grundlegend Falsches daran, zwei verschiedene Strategien für die Verwaltung einer fehlgeschlagenen Prüfung zu haben. Ich würde es vorziehen, die Zusammensetzung zu verwenden, um zu bestimmen, welche Strategie verwendet wird. Das Feature-Flag wird im Kompositionsstamm und nicht bei der Implementierung der Bibliotheksmethode verwendet.

// Configurable dependencies
AreaCalculator(PreconditionFailureStrategy strategy)

CalculateArea(int x, int y)
{
    if (x < 0 || y < 0) {
        return this.strategy.fail(0);
    }
    // ...
}

Sie haben an kritischen Anwendungen für die Luftfahrt gearbeitet, bei denen das System nicht ausfallen konnte.

Das National Traffic and Safety Board ist wirklich gut; Ich könnte den Graubärten alternative Implementierungstechniken vorschlagen, aber ich bin nicht geneigt, mit ihnen über das Entwerfen von Schotten im Fehlerberichtssubsystem zu streiten.

Allgemeiner: Was kostet das Unternehmen? Es ist viel billiger, eine Website zum Absturz zu bringen, als ein lebenskritisches System.

0
VoiceOfUnreason

Es gibt Zeiten, in denen Sie seinen Ansatz verfolgen möchten, aber ich würde sie nicht als "normale" Situation betrachten. Der Schlüssel, um festzustellen, in welchem ​​Fall Sie sich befinden, ist:

Seine Logik/Argumentation ist, dass unser Programm eine Sache tun muss, um dem Benutzer Daten zu zeigen. Jede andere Ausnahme, die uns nicht davon abhält, sollte ignoriert werden.

Überprüfen Sie die Anforderungen. Wenn Ihre Anforderungen tatsächlich besagen, dass Sie einen Job haben, nämlich dem Benutzer Daten anzuzeigen, hat er Recht. Nach meiner Erfahrung kümmert sich der Benutzer auch meistens darum, welche Daten angezeigt werden. Sie wollen die richtigen Daten. Einige Systeme möchten nur leise ausfallen und einen Benutzer herausfinden lassen, dass ein Fehler aufgetreten ist, aber ich würde sie als Ausnahme von der Regel betrachten.

Die Schlüsselfrage, die ich nach einem Fehler stellen würde, lautet: "Befindet sich das System in einem Zustand, in dem die Erwartungen des Benutzers und die Invarianten der Software gültig sind?" Wenn ja, dann kehren Sie auf jeden Fall zurück und machen Sie weiter. In der Praxis ist dies in den meisten Programmen nicht der Fall.

Was das Flag selbst betrifft, wird das Ausnahmeflag normalerweise als Codegeruch betrachtet, da ein Benutzer irgendwie wissen muss, in welchem ​​Modus sich das Modul befindet, um zu verstehen, wie die Funktion funktioniert. Wenn es in !shouldThrowExceptions Modus muss der Benutzer wissen, dass sie dafür verantwortlich sind, Fehler zu erkennen und Erwartungen und Invarianten aufrechtzuerhalten, wenn sie auftreten. Sie sind auch sofort an der Zeile verantwortlich, an der die Funktion aufgerufen wird. Eine solche Flagge ist normalerweise sehr verwirrend.

Es kommt jedoch vor. Beachten Sie, dass viele Prozessoren das Ändern des Gleitkommaverhaltens innerhalb des Programms zulassen. Ein Programm, das entspanntere Standards wünscht, kann dies einfach durch Ändern eines Registers (das praktisch eine Flagge ist) tun. Der Trick ist, dass Sie sehr vorsichtig sein sollten, um nicht versehentlich auf andere Zehen zu treten. Der Code überprüft häufig das aktuelle Flag, setzt es auf die gewünschte Einstellung, führt Operationen aus und setzt es dann zurück. Auf diese Weise wird niemand von der Veränderung überrascht.

0
Cort Ammon