Wir haben eine Flagge, die anzeigt, ob wir Fehler werfen sollen


64

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

Er neigt dazu, einen Booleschen Wert in die Objekte einzufügen, um anzuzeigen, 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 versucht wird, ein Netzwerkgerät zu verwenden, das zurzeit nicht vorhanden sein kann. Das Bereichsbeispiel ist nur ein Beispiel für das Ausnahmeflag.)

Für mich scheint das ein Codegeruch zu sein. Das Schreiben von Komponententests wird etwas komplexer, da Sie jedes Mal auf das Ausnahmeflag prüfen müssen. Wenn etwas schief geht, möchten Sie es dann nicht sofort wissen? Sollte es nicht die Verantwortung des Anrufers sein, festzulegen, wie fortgefahren werden soll?

Seine Logik / Argumentation ist, dass unser Programm 1 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 gejagt werden und von der entsprechenden Person gehandhabt werden sollten und dass sie dafür nicht mit Flaggen umgehen müssen.

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

Bearbeiten : Um mehr Kontext über die Entwurfsentscheidung zu geben, vermute ich, dass dies daran liegt, dass das Programm bei einem Ausfall dieser Komponente weiterhin ausgeführt werden und seine Hauptaufgabe erfüllen kann. Daher möchten wir keine Ausnahme auslösen (und nicht damit umgehen?) Und das Programm herunterfahren, wenn es für den Benutzer einwandfrei funktioniert

Edit 2 : Um noch mehr Kontext zu geben, 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. Daher löst Zurücksetzen eine Ausnahme aus, da wir versuchen würden, die Hardware mit der alten IP zurückzusetzen.


22
c # hat eine Konvention für dieses Try-Parse-Muster. Weitere Informationen: docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… Die Markierung stimmt nicht mit diesem Muster überein.
Peter

18
Dies ist im Grunde genommen ein Steuerparameter und ändert, wie die internen Methoden ausgeführt werden. Das ist ungeachtet des Szenarios schlecht. martinfowler.com/bliki/FlagArgument.html , softwareengineering.stackexchange.com/questions/147977/... , medium.com/@amlcurran/...
bic

1
Zusätzlich zum Try-Parse-Kommentar von Peter gibt es hier einen schönen Artikel über Vexing-Ausnahmen: blogs.msdn.microsoft.com/ericlippert/2008/09/10/…
Linaith

2
"Wir möchten also keine Ausnahme auslösen (und nicht damit umgehen?) Und das Programm herunterfahren, wenn es für den Benutzer einwandfrei funktioniert." - Sie wissen, dass Sie Ausnahmen abfangen können, oder?
user253751

1
Ich bin mir ziemlich sicher, dass dies zuvor an anderer Stelle behandelt wurde, aber angesichts des einfachen Bereichsbeispiels würde ich mich eher fragen, wo diese negativen Zahlen herkommen und ob Sie in der Lage sind, diese Fehlerbedingung an einer anderen Stelle zu behandeln (z. B. was auch immer die Datei mit Länge und Breite las, zum Beispiel); "Wir versuchen jedoch, ein Netzwerkgerät zu verwenden, das zurzeit nicht vorhanden sein kann." point könnte eine ganz andere Antwort verdienen. Ist dies eine Drittanbieter-API oder ein Industriestandard wie TCP / UDP?
JRH

Antworten:


74

Das Problem bei diesem Ansatz besteht darin, dass Ausnahmen zwar nie ausgelöst werden (und die Anwendung daher nie 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 (oder) vorliegt was das Problem ist und wie man es behebt).

Damit die Ergebnisse korrekt und aussagekräftig sind, muss die aufrufende Methode das Ergebnis auf spezielle Zahlen überprüfen, dh auf bestimmte Rückgabewerte, die verwendet werden, um Probleme anzuzeigen, 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. Die Daten werden dann dem Benutzer mit einem Bereich von 0 angezeigt, von dem der Benutzer weiß, dass er falsch ist, aber er hat keinen Hinweis darauf, was wo 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. Eine ordnungsgemäße Ausnahmebehandlung (und das Testen!) Stellen sicher, dass kritische Anwendungen nicht abstürzen oder auf andere Weise in einem ungültigen Zustand beendet werden.


1
@Quirk Es ist beeindruckend, wie Chen es geschafft hat, das Prinzip der einfachen Verantwortung in nur drei oder vier Zeilen zu verletzen. Das ist das eigentliche Problem. Außerdem ist das Problem, von dem er spricht (der Programmierer denkt nicht über die Konsequenzen von Fehlern in jeder Zeile nach), immer eine Möglichkeit mit ungeprüften Ausnahmen und nur manchmal eine Möglichkeit mit überprüften Ausnahmen. Ich glaube, ich habe alle Argumente gesehen, die jemals gegen geprüfte Ausnahmen vorgebracht wurden, und keines davon ist gültig.
TKK

@TKK persönlich gibt es einige Fälle, in denen ich gerne Ausnahmen in .NET überprüft hätte. Es wäre schön, wenn es einige statische High-End-Analysetools geben würde, die sicherstellen könnten, dass die API-Dokumente, die als Ausnahmen geworfen werden, korrekt sind. Dies ist jedoch wahrscheinlich nahezu unmöglich, insbesondere beim Zugriff auf native Ressourcen.
JRH

1
@jrh Ja, es wäre schön, wenn etwas Ausnahmesicherheit in .NET enthalten würde, ähnlich wie TypeScript Sicherheit in JS enthält.
TKK

47

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

Nein, ich denke das ist eine ziemlich schlechte Übung. Das Auslösen einer Ausnahme im Vergleich zum Zurückgeben eines Werts ist eine grundlegende Änderung der API, die Änderung der Methodensignatur und das Verhalten der Methode aus Sicht der Benutzeroberfläche.

Im Allgemeinen sollten wir dies berücksichtigen, wenn wir Klassen und ihre APIs entwerfen

  1. Es können mehrere Instanzen der Klasse mit unterschiedlichen Konfigurationen im selben Programm zur selben Zeit schweben.

  2. Aufgrund der Abhängigkeitsinjektion und einer beliebigen Anzahl anderer Programmierpraktiken kann ein konsumierender Client die Objekte erstellen und an einen anderen Benutzer übergeben. Daher gibt es häufig eine Trennung zwischen Objekterstellern und Objektbenutzern.

Überlegen Sie sich jetzt, was der Methodenaufrufer tun muss, um eine Instanz zu nutzen, die ihm übergeben wurde, z. B. um die Berechnungsmethode aufzurufen: Der Aufrufer müsste sowohl prüfen, ob der Bereich Null ist, als auch Ausnahmen abfangen - autsch! Testüberlegungen betreffen nicht nur die Klasse selbst, sondern auch die Fehlerbehandlung der Aufrufer ...

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 (vielleicht Sie oder Ihr Kollege) in die Erfolgsgrube gerät.

Um beide APIs anbieten zu können, sind Sie viel besser und normaler, wenn Sie entweder zwei verschiedene Klassen bereitstellen - eine, die immer einen Fehler auslöst und eine, die immer einen Fehler von 0 zurückgibt, oder zwei verschiedene Methoden mit einer einzelnen Klasse bereitstellen. Auf diese Weise kann der konsumierende Client auf einfache Weise genau nach Fehlern suchen und diese behandeln.

Mit zwei verschiedenen Klassen oder zwei verschiedenen Methoden können Sie die IDEs verwenden, um Methodenbenutzer und Refactor-Features usw. leichter zu finden, da die beiden Anwendungsfälle nicht mehr miteinander in Konflikt stehen. Das Lesen, Schreiben, Warten, Überprüfen und Testen von Code ist ebenfalls einfacher.


Ich persönlich bin der Meinung, dass wir keine booleschen Konfigurationsparameter verwenden sollten, bei denen die tatsächlichen Aufrufer alle einfach eine Konstante übergeben . Bei einer solchen Konfigurationsparametrisierung werden zwei separate Anwendungsfälle ohne wirklichen Nutzen zusammengeführt.

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


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, können Sie möglicherweise verschiedene Klassen zur Unterscheidung dieser Zustände verwenden (nicht berechenbarer Bereich vs. berechenbarer Bereich).

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


Sollte es nicht die Verantwortung des Anrufers sein, festzulegen, wie fortgefahren werden soll?

Ja, ich stimme zu. Es scheint verfrüht für den Angerufenen zu sein, zu entscheiden, dass der Bereich 0 unter Fehlerbedingungen die richtige Antwort ist (zumal 0 ein gültiger Bereich ist, sodass der Unterschied zwischen dem Fehler und dem tatsächlichen Wert 0 möglicherweise nicht für Ihre App gilt).


Sie müssen die Ausnahme nicht wirklich überprüfen, da Sie die Argumente überprüfen müssen, bevor Sie die Methode aufrufen. Das Überprüfen des Ergebnisses gegen Null unterscheidet nicht zwischen den zulässigen Argumenten 0, 0 und den unzulässigen negativen Argumenten. Die API ist meiner Meinung nach wirklich schrecklich.
Blackjack

Die Annex K MS-Pushs für C99- und C ++ - Iostreams sind Beispiele für APIs, bei denen ein Hook oder ein Flag die Reaktion auf Fehler radikal ändert.
Deduplizierer

37

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 Zusammenhängen keine Ausnahmen zu werfen, "weil das System dann ausfallen könnte" . Aber wenn das System "wegen einer Ausnahme ausfallen kann", ist dies ein klarer Hinweis darauf

  • Ausnahmen werden nicht richtig gehandhabt , zumindest nicht starr.

Wenn das Programm, das das verwendet AreaCalculator, fehlerhaft ist, möchte Ihr Kollege nicht, dass das Programm "früh abstürzt", sondern einen falschen Wert zurückgibt (in der Hoffnung, dass es niemand bemerkt oder dass niemand etwas Wichtiges damit tut). Das maskiert tatsächlich einen Fehler und wird meiner Erfahrung nach früher oder später zu Folge-Fehlern 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, in keiner Weise 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, zu entscheiden, ob der Benutzer über das falsche Verhalten informiert werden muss und ob es sicher ist, eine Verarbeitung fortzusetzen, oder ob es sicherer ist das Programm vollständig zu stoppen. Daher würde ich eines der folgenden empfehlen:

  • Machen Sie es sich schwer, die Tatsache 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, um Ausnahmen zu erwarten und damit umzugehen, wenn es "Black Box" -Komponenten verwendet und das globale Verhalten des Programms im Auge hat.

  • Wenn Sie glauben, dass der aufrufende Code (oder die Entwickler, die ihn schreiben) aus bestimmten Gründen nicht in der Lage sind, die Ausnahmebehandlung ordnungsgemäß zu verwenden, können Sie als letzte Möglichkeit eine API mit expliziten Fehlerausgabevariablen und überhaupt keinen Ausnahmen wie erstellen

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

    Daher wird es für den Anrufer sehr schwierig, die Funktion zu übersehen. 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, daran zu arbeiten.


3
"Ein Programm zu schreiben, das unter keinen Umständen abstürzt, aber falsche Daten oder Berechnungsergebnisse anzeigt, ist in der Regel keineswegs besser, als das Programm abstürzen zu lassen", stimme ich im Allgemeinen zu, obwohl ich mir vorstellen könnte, dass ich in der Luftfahrt wahrscheinlich das Flugzeug vorziehen würde immer noch mit den Instrumenten, die falsche Werte im Vergleich zum Herunterfahren des Flugzeugcomputers anzeigen. Für alle weniger kritischen Anwendungen ist es definitiv besser, Fehler nicht zu maskieren.
Trilarion

18
@Trilarion: Wenn ein Programm für Flugcomputer keine ordnungsgemäße Ausnahmebehandlung enthält, ist das "Korrigieren", indem Komponenten keine Ausnahmen auslösen, ein sehr fehlgeleiteter Ansatz. Wenn das Programm abstürzt, sollte es ein redundantes Backup-System geben, das es übernehmen kann. Wenn das Programm nicht abstürzt und beispielsweise eine falsche Höhe anzeigt, denken die Piloten möglicherweise, dass "alles in Ordnung ist", während das Flugzeug in den nächsten Berg stürzt.
Doc Brown

7
@Trilarion: Wenn der Flugcomputer eine falsche Höhe anzeigt und das Flugzeug aus diesem Grund abstürzt, hilft es Ihnen auch nicht (insbesondere, wenn das Backup-System vorhanden ist und nicht informiert wird, dass es übernehmen muss). Sicherungssysteme für Flugzeugcomputer sind keine neue Idee, google für "Flugzeugcomputer-Sicherungssysteme". Ich bin mir ziemlich sicher, dass Ingenieure auf der ganzen Welt immer redundante Systeme in jedes lebenswichtige System eingebaut haben (und wenn es nur darum ginge, nicht zu verlieren) Versicherung).
Doc Brown

4
Diese. Wenn Sie es sich nicht leisten können, dass das Programm abstürzt, können Sie es sich auch nicht leisten, dass es im Stillen falsche Antworten gibt. Die richtige Antwort ist in jedem Fall eine angemessene Ausnahmebehandlung . Für eine Website bedeutet dies, dass ein globaler Handler unerwartete Fehler in 500 konvertiert. Sie verfügen möglicherweise auch über zusätzliche Handler für spezifischere Situationen, z. B. ein try/ catchin einer Schleife, wenn die Verarbeitung fortgesetzt werden muss, wenn ein Element ausfällt.
jpmc26

2
Das falsche Ergebnis zu erzielen, ist immer das Schlimmste. Das erinnert mich an die Regel zur Optimierung: "Richtig machen, bevor Sie optimieren, denn eine schnellere falsche Antwort nützt immer noch niemandem."
Toby Speight

13

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

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

Während es eine großartige Idee ist, Code zu schreiben, der leicht 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 Anrufer, die dieses Verhalten wünschen, es verdient haben, ihn zur Funktion hinzuzufügen. Wo die Grenze zwischen komplex und zu komplex liegt, ist ein Urteilsspruch; Jeder, der versucht, Ihnen mitzuteilen, dass es eine helle Linie gibt, die in allen Situationen zutrifft, 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 shouldThrowExceptionsist wahr". Eine Dimension zu erhalten, wenn kleiner als Null ist, ist nicht falsch, wenn shouldThrowExceptionsfalsch ist, weil der Schalter ein anderes Verhalten hervorruft. Das ist ganz einfach keine Ausnahmesituation.

Das eigentliche Problem hierbei ist, dass der Switch einen schlechten Namen hat, da er nicht beschreibt, was die Funktion bewirkt. Hätte man ihm einen besseren Namen gegeben treatInvalidDimensionsAsZero, hättest du diese Frage gestellt?

Sollte es nicht die Verantwortung des Anrufers sein, festzulegen, wie fortgefahren werden soll?

Der Anrufer legt fest, wie fortgefahren werden soll. In diesem Fall erfolgt dies vorzeitig durch Setzen oder Löschen, shouldThrowExceptionsund 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, können Ausnahmen zu Problemen für Anrufer führen, die sie nicht lösen können. Wenn ich eine Liste von übergebe [5, 6, -1, 8, 12]und die Funktion eine Ausnahme über das -1auslöst, kann ich 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, eine Kopie ohne negative Zahlen zu erstellen, bevor die Funktion aufgerufen wird. Daher muss ich im Voraus festlegen, wie ungültige Zahlen behandelt werden sollen, entweder in Form eines "Ignorieren Sie sie einfach "wechseln oder vielleicht ein Lambda bereitstellen, das aufgerufen wird, um diese Entscheidung zu treffen.

Seine Logik / Argumentation ist, dass unser Programm 1 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 gejagt werden und von der entsprechenden Person gehandhabt werden sollten und dass sie dafür nicht mit Flaggen umgehen müssen.

Auch hier gibt es keine Einheitslösung. In dem Beispiel wurde die Funktion vermutlich auf eine Spezifikation geschrieben, die besagt, 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 Meldungen füllen, die besagen: "Normalerweise würde hier eine Ausnahme ausgelöst, aber der Anrufer sagte, er solle sich nicht darum kümmern."

Und als einer dieser viel älteren Programmierer möchte ich Sie bitten, sich freundlich von meinem Rasen zu entfernen. ;-)


Ich bin damit einverstanden, dass die Benennung und die Absicht extrem wichtig sind und in diesem Fall der richtige Parametername wirklich den Spieß umdrehen kann, also +1, aber 1. die our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignoredDenkweise kann dazu führen, dass der Benutzer eine Entscheidung auf der Grundlage der falschen Daten trifft (weil das Programm dies tatsächlich tun muss) 1 Sache - um dem Benutzer zu helfen, fundierte Entscheidungen zu treffen) 2. Ähnliche Fälle wie bool ExecuteJob(bool throwOnError = false)sind normalerweise extrem fehleranfällig und führen zu Code, über den man nur schwer nachdenken kann, wenn man ihn liest.
Eugene Podskal

@EugenePodskal Ich denke, die Annahme ist, dass "show data" "show correct data" bedeutet. Der Fragesteller sagt nicht, dass das fertige Produkt nicht funktioniert, nur, dass es möglicherweise "falsch" geschrieben ist. Ich müsste einige harte Daten zum zweiten Punkt sehen. Ich habe eine Handvoll häufig verwendeter Funktionen in meinem aktuellen Projekt, die einen Wurf / No-Throw-Schalter haben und über die man nicht schwerer nachdenken kann als über jede andere Funktion, aber das ist ein Datenpunkt.
Blrfl

Gute Antwort, ich denke, diese Logik gilt für eine viel größere Bandbreite von Umständen als nur für die OP. Übrigens, cpp's new hat aus genau diesen Gründen eine Throw / No-Throw-Version. Ich weiß, dass es ein paar kleine Unterschiede gibt, aber ...
drjpizzle

8

Sicherheitskritischer und „normaler“ Code kann zu sehr unterschiedlichen Vorstellungen darüber führen, wie „gute Praxis“ aussieht. Es gibt viele Überschneidungen - manche Dinge sind riskant und sollten in beiden Fällen vermieden werden - aber es gibt immer noch signifikante Unterschiede. Wenn Sie eine Anforderung hinzufügen, um garantiert reaktionsfähig zu sein, 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 bleiben oder sogar abstürzen (was praktisch keine Probleme in Bezug auf das versehentliche Ändern des eingecheckten Codes sind).

    Jedoch: Bei einer Instrumententafel mit einer Beschleunigungskraftberechnung kann das Abwürgen und Verhindern einer Luftgeschwindigkeitsberechnung nicht akzeptabel sein.

Einige sind weniger offensichtlich:

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

  • Es ist relativ wichtig, nachweislich sicher zu sein. Nicht viele Kunden werden sich darüber Gedanken machen, 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 normaleren Situationen ziemlich albern wären.

Einige beziehen sich auf eingebettet zu sein, einige auf Sicherheit und vielleicht andere ... Einige sind gut (enge Leistung / 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 Unbekannten auf dem Gebiet des Software-Engineering-Stack-Austauschs getroffen werden. Es kann durchaus einen guten Grund dafür geben, auch wenn es Teil eines schlechten Systems ist. Lesen Sie nicht zu viel in irgendetwas anderes als als "Denkanstoß".


7

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 verfügt über genügend Status, um alle glücklichen Pfade und alle unglücklichen Pfade zu beschreiben. Der Punkt ist, wenn Sie mit dieser Funktion interagieren (member / global / sonst), werden Sie gezwungen, das Ergebnis zu behandeln.

Davon abgesehen sollte dieser angereicherte Datentyp keine Aktion erzwingen. Stellen Sie sich in Ihrer Nähe beispielsweise so etwas vor var area_calc = new AreaCalculator(); var volume = area_calc.CalculateArea(x, y) * z;. Scheint nützlich, volumesollte 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 wurde area_calc .CalculateArea(x, y)ein Rich-Datentyp zurückgegeben, der einen Fehler enthielt. Ist es legal, das mit zu multiplizieren z? Das ist eine gute Frage. Sie können Benutzer dazu zwingen, die Überprüfung sofort durchzuführen. Dies unterbricht jedoch die Logik bei der Fehlerbehandlung.

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 Logik ist über zwei Zeilen verteilt und wird im ersten Fall durch Fehlerbehandlung geteilt, während im zweiten Fall die gesamte relevante Logik auf einer Zeile steht, gefolgt von Fehlerbehandlung.

In diesem zweiten Fall volumehandelt es sich um einen Rich-Datentyp. Es ist nicht nur eine Zahl. Dies vergrößert den Speicher und volumemuss noch auf Fehler untersucht werden. Darüber hinaus kann volumees vorkommen, dass andere Berechnungen eingegeben werden, bevor der Benutzer den Fehler behandelt, sodass er sich an mehreren unterschiedlichen Stellen manifestiert. Dies kann je nach Situation gut oder schlecht sein.

Alternativ kann volumees sich auch nur um einen einfachen Datentyp handeln - nur um eine Zahl. 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 kann dies sinnvoll erscheinen). 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
}

gegen

... 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 ist der Benutzer sehr verpflichtet, die Daten zu 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 feststellen, wenn etwas schief geht. Der zweite Fall kombiniert 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.

Um genau zu sein, ist ein unglücklicher Pfad ein Fall, der der Geschäftslogik (der Domäne der Ausnahmen) unbekannt ist. Wenn Sie nicht validieren, ist dies ein glücklicher Pfad, da Sie wissen, wie Sie mit den Geschäftsregeln (der Domäne der Regeln) umgehen müssen.

Die ultimative Lösung wäre eine, die alle Szenarien (im Rahmen der Vernunft) erlaubt.

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

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

@TobySpeight Fair genug, diese Dinge sind kontextsensitiv und haben ihre Reichweite.
Kain0_0

Ich denke, dass das Problem hier die 'assert_not_bad'-Blöcke sind. Ich denke, diese werden am selben Ort enden wie der ursprüngliche Code, der versucht wurde, zu lösen. Beim Testen müssen diese jedoch beachtet werden, wenn es sich tatsächlich um Aussagen handelt, die vor der Produktion in einem echten Flugzeug entfernt werden sollten. ansonsten ein paar tolle punkte.
Drjpizzle

@drjpizzle Ich würde argumentieren, dass es wichtig genug ist, einen Guard zum Testen hinzuzufügen, um den Guard in der Produktion an Ort und Stelle zu lassen. Die Anwesenheit der Wache selbst impliziert Zweifel. Wenn Sie den Code so stark bezweifeln, dass er während des Tests geschützt ist, bezweifeln Sie dies aus technischen Gründen. Dh der Zustand könnte / tut sich realistisch. Das Ausführen von Tests beweist nicht, dass der Zustand in der Produktion niemals erreicht wird. Das bedeutet, dass es einen bekannten Zustand gibt, der auftreten könnte und irgendwie behandelt werden muss. Ich denke, wie es gehandhabt wird, ist das Problem.
Kain0_0

3

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, da Ausnahmebehandlung ist schwer - den Fehlerfall macht langsam laufen und macht den Fehlerfall Speicher zuweisen (die manchmal nicht einmal verfügbar ist), und in der Regel macht die Dinge weniger vorhersehbar. Die Schwerfälligkeit von EH ist einer der Gründe, warum Sie Leute sagen hören: "Verwenden Sie keine Ausnahmen für den Kontrollfluss."

So verwenden einige Bibliotheken (wie <filesystem>), was C ++ eine "duale API" nennt, oder was C # das Try-ParseMuster nennt (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 schwere Wahl zwischen nützlichen Fehlermeldungen ( CalculateArea) und ( ) treffen speed ( TryCalculateArea) weil die schnellere Version unsere nützliche "negative side lengths"Ausnahme in eine unbrauchbare reduziert false- "etwas ist schief gelaufen, frag mich nicht was oder wo." (Einiger Dual - APIs verwenden , um einen aussagekräftigen Fehlertyp, wie zum Beispiel int errnooder C ++ 's std::error_code, aber das noch nicht , dass Ihnen nicht sagen , wo der Fehler aufgetreten ist - nur , dass es tat irgendwo auftreten.)

Wenn Sie nicht entscheiden können, wie sich Ihr Code verhalten soll, können Sie die Entscheidung immer 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 "error handler" in eine globale Variable ausklammert:

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 Status ist fast immer eine schlechte Idee. Ich kann es nicht empfehlen. (Die Tatsache , dass es nicht global Zustand in Ihrem Fall aber einfach Instanz weite Mitgliedsstaat mildert die Schlechtigkeit ein wenig, aber nicht viel.)

Darüber hinaus schränkt Ihr Mitarbeiter die Anzahl möglicher Fehlerbehandlungsverhalten unnötig ein. Anstatt zu erlauben jede Fehlerbehandlung Lambda, er ist auf 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 du hast alle Probleme eines gemeinsamen globalen Staates; und Sie bezahlen immer noch überall für diesen bedingten Zweig.

Eine gängige Lösung in C ++ (oder einer beliebigen Sprache mit bedingter Kompilierung) wäre schließlich, den Benutzer zu zwingen, die Entscheidung für sein gesamtes Programm global zur Kompilierungszeit zu treffen, damit der nicht genommene 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 assertMakro in C und C ++, das sein Verhalten auf dem Präprozessor-Makro festlegt NDEBUG.


Wenn man stattdessen ein std::optionalvon zurückgibt TryCalculateArea(), ist es einfach, die Implementierung beider Teile der dualen Schnittstelle in einer einzigen Funktionsvorlage mit einem Kompilierungszeit-Flag zu vereinheitlichen.
Deduplicator

@ Deduplicator: Vielleicht mit einem std::expected. Mit nur std::optional, wenn ich Ihre vorgeschlagene Lösung nicht falsch verstehe, würde es immer noch darunter leiden, was ich gesagt habe: Der Benutzer muss eine schwierige Wahl zwischen nützlichen Fehlermeldungen und Geschwindigkeit treffen, weil die schnellere Version unsere nützliche "negative side lengths"Ausnahme nimmt und sie zu einer nutzlosen abflacht false- " etwas ist schief gelaufen, frag mich nicht was oder wo. "
Quuxplusone

Das ist der Grund, warum libc ++ <Dateisystem> dem Muster von OPs Mitarbeitern sehr nahe kommt: Es leitet den std::error_code *ecgesamten Weg durch alle Ebenen der API und im unteren Bereich das moralische Äquivalent von if (ec == nullptr) throw something; else *ec = some error code. (Es abstrahiert das Tatsächliche ifin etwas ErrorHandler
Genanntes

Nun, das wäre eine Option, um die erweiterten Fehlerinformationen zu behalten, ohne sie zu werfen. Ist möglicherweise angemessen oder die potenziellen zusätzlichen Kosten nicht wert.
Deduplizierer

1
So viele gute Gedanken in dieser Antwort enthalten ... Benötigt auf jeden Fall mehr Upvotes :-)
cmaster

1

Ich finde, 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 gültig ist. (Zum Beispiel sind alle intWerte 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 outSchlüsselwort mussten Sie entweder eine Ausnahme auslösen (teuer, und der Aufrufer muss sie abfangen oder das gesamte Programm beenden) und für jedes Ergebnis eine spezielle Struktur erstellen (Klasse vor Klasse oder Generika waren wirklich eine Sache), um den Wert darzustellen und mögliche Fehler (zeitaufwendiges Erstellen und Aufblähen von Software) oder Rückgabe eines Standardwerts für "error" (der möglicherweise kein Fehler war).

Der Ansatz, den Ihre Kollegen verwenden, gibt ihnen den Vorteil, dass Ausnahmen beim Testen / Debuggen neuer Funktionen frühzeitig fehlschlagen, während sie gleichzeitig die Laufzeitsicherheit und -leistung (Leistung war vor etwa 30 Jahren immer ein kritisches Problem) erhalten, nur einen Standardfehlerwert zurückzugeben. Dies ist das Muster, in das die Software geschrieben wurde, und das erwartete Muster, das sich weiterentwickelt. Daher ist es natürlich, dies auch weiterhin so zu tun, obwohl es jetzt bessere Möglichkeiten gibt. Höchstwahrscheinlich stammt dieses Muster aus dem Zeitalter der Software oder einem Muster, aus dem Ihre Colleges nie hervorgegangen sind (alte Gewohnheiten sind schwer zu brechen).

In den anderen Antworten wird bereits erläutert, warum dies als schlechte Vorgehensweise angesehen wird. Ich empfehle Ihnen daher, das TryGet-Muster nachzulesen (möglicherweise auch die Verkapselung der Versprechungen, die ein Objekt dem Aufrufer machen sollte).


Vor dem outSchlüsselwort schreiben Sie eine boolFunktion, die auf das Ergebnis zeigt, dh einen refParameter. Sie können dies in VB6 im Jahr 1998 tun. Das outSchlüsselwort gibt Ihnen lediglich die Sicherheit beim Kompilieren, dass der Parameter zugewiesen ist, wenn die Funktion zurückgegeben wird. Es ist ein schönes und nützliches Muster.
Mathieu Guindon

@MathieuGuindon Yeay, aber GetTry war noch kein bekanntes / etabliertes Muster, und selbst wenn, bin ich mir nicht ganz sicher, ob es verwendet worden wäre. Schließlich war ein Teil des Vorlaufs bis zum Jahr 2000, dass die Speicherung von Werten über 0-99 inakzeptabel war.
Tezra

0

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 1 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, der darin besteht, dem Benutzer Daten anzuzeigen, dann hat er Recht. Nach meiner Erfahrung ist es dem Benutzer jedoch in den meisten Fällen auch wichtig, welche Daten angezeigt werden. Sie wollen die richtigen Daten. Einige Systeme möchten nur leise ausfallen und einen Benutzer feststellen lassen, dass etwas schief gelaufen 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 kehre einfach zurück und mach 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 ist !shouldThrowExceptionsModus muss der Benutzer wissen , dass sie für die Aufdeckung von Fehlern verantwortlich sind und die Aufrechterhaltung Erwartungen und Invarianten , wenn sie auftreten. Sie sind auch genau dort zuständig, wo die Funktion aufgerufen wird. Eine solche Flagge ist normalerweise sehr verwirrend.

Es kommt jedoch vor. Bedenken Sie, dass viele Prozessoren das Ändern des Gleitkommaverhaltens innerhalb des Programms zulassen. Ein Programm, das entspanntere Standards haben möchte, 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. Code überprüft häufig das aktuelle Flag, setzt es auf die gewünschte Einstellung, führt Vorgänge aus und setzt es dann zurück. Auf diese Weise wird niemand von der Veränderung überrascht.


0

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 eine Voraussetzungsprüfung . Eine nicht bestandene Voraussetzungsprüfung impliziert einen höheren Fehler 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 ist darauf zurückzuführen, dass diese Grenzfläche primitive Besessenheit zeigt - xund yvermutlich reale Längenmaße darstellen soll. In einem Programmierkontext, in dem domänenspezifische Typen eine vernünftige Wahl sind, würden wir die Voraussetzungsprüfung tatsächlich näher an die Datenquelle heranrü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 grundsätzlich Falsches daran, zwei verschiedene Strategien für die Verwaltung eines fehlgeschlagenen Schecks zu haben. Ich würde es vorziehen, Komposition zu verwenden, um zu bestimmen, welche Strategie angewendet wird. Das Feature-Flag wird in der Kompositionswurzel und nicht in 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 würde den Graubärten vielleicht alternative Implementierungstechniken vorschlagen, aber ich bin nicht geneigt, mit ihnen über das Entwerfen von Trennwänden im Fehlerberichts-Subsystem zu streiten.

Ganz allgemein: Was kostet das Geschäft? Es ist viel billiger, eine Website zum Absturz zu bringen, als ein lebenswichtiges System.


Mir gefällt der Vorschlag einer alternativen Möglichkeit, die gewünschte Flexibilität beizubehalten und gleichzeitig zu modernisieren.
Drjpizzle

-1

Methoden behandeln entweder Ausnahmen oder nicht, in Sprachen wie C # sind keine Flags erforderlich.

public int Method1()
{
  ...code

 return 0;
}

Wenn in ... Code etwas schief geht, muss diese Ausnahme vom Aufrufer behandelt werden. Wenn niemand den Fehler behandelt, wird das Programm beendet.

public int Method1()
{
try {  
...code
}
catch {}
 ...Handle error 
}
return 0;
}

In diesem Fall behandelt Methode1 das Problem, wenn in ... Code etwas Schlimmes passiert, und das Programm sollte fortgesetzt werden.

Wo Sie mit Ausnahmen umgehen, liegt bei Ihnen. Sicherlich können Sie sie ignorieren, indem Sie abfangen und nichts tun. Aber ich würde sicherstellen, dass Sie nur bestimmte Arten von Ausnahmen ignorieren, die Sie erwarten können. Ignorieren ( exception ex) ist gefährlich, da einige Ausnahmen, die Sie nicht ignorieren möchten, wie Systemausnahmen bezüglich zu wenig Arbeitsspeicher und dergleichen.


3
Das aktuelle Setup, das OP veröffentlicht hat, bezieht sich auf die Entscheidung, ob eine Ausnahme gewollt ausgelöst werden soll. OPs Code führt nicht zu ungewolltem Verschlucken von Dingen wie Ausnahmen aufgrund von unzureichendem Arbeitsspeicher. Wenn überhaupt, impliziert die Behauptung, dass Ausnahmen das System zum Absturz bringen, dass die Codebasis keine Ausnahmen abfängt und somit keine Ausnahmen verschluckt; beide, die absichtlich von OPs Geschäftslogik geworfen wurden und nicht.
Flater

-1

Dieser Ansatz bricht die Philosophie "schnell scheitern, hart scheitern".

Warum Sie schnell scheitern wollen :

  • Je schneller Sie ausfallen, desto näher ist das sichtbare Symptom des Ausfalls an der tatsächlichen Ursache des Ausfalls. Dies erleichtert das Debuggen erheblich - im besten Fall steht die Fehlerzeile direkt in der ersten Zeile Ihres Stack-Trace.
  • Je schneller Sie versagen (und den Fehler entsprechend abfangen), desto unwahrscheinlicher ist es, dass Sie den Rest Ihres Programms verwirren.
  • Je schwerer Sie versagen (dh eine Ausnahme auslösen, anstatt nur einen "-1" -Code oder ähnliches zurückzugeben), desto wahrscheinlicher ist es, dass sich der Aufrufer tatsächlich um den Fehler kümmert und nicht einfach mit falschen Werten weiterarbeitet.

Nachteile, wenn man nicht schnell und hart versagt:

  • Wenn Sie sichtbare Fehler vermeiden, dh vorgeben, dass alles in Ordnung ist, ist es für Sie in der Regel unglaublich schwierig, den tatsächlichen Fehler zu finden. Stellen Sie sich vor, der Rückgabewert Ihres Beispiels ist Teil einer Routine, die die Summe von 100 Bereichen berechnet. Das heißt, diese Funktion wird 100 Mal aufgerufen und die Rückgabewerte werden summiert. Wenn Sie den Fehler stillschweigend unterdrücken, können Sie nicht herausfinden, wo der eigentliche Fehler auftritt. und alle folgenden Berechnungen werden stillschweigend falsch sein.
  • Wenn Sie einen Fehler verzögern (indem Sie einen unmöglichen Rückgabewert wie "-1" für einen Bereich zurückgeben), erhöhen Sie die Wahrscheinlichkeit, dass sich der Aufrufer Ihrer Funktion nicht darum kümmert und vergisst, den Fehler zu behandeln. obwohl sie die Informationen über den Fehler zur Hand haben.

Schließlich hat die eigentliche ausnahmebasierte Fehlerbehandlung den Vorteil, dass Sie eine "Out-of-Band" -Fehlermeldung oder ein "Out-of-Band" -Objekt ausgeben können. Sie können das Protokollieren von Fehlern, Warnungen usw. problemlos einbinden, ohne eine einzige zusätzliche Zeile in Ihren Domain-Code schreiben zu müssen .

Es gibt also nicht nur einfache technische Gründe, sondern auch "Systemgründe", die einen schnellen Ausfall sehr nützlich machen.

Letztendlich ist es in unserer heutigen Zeit, in der die Ausnahmebehandlung leicht und sehr stabil ist, nur halbwegs kriminell, nicht hart und schnell zu scheitern. Ich verstehe sehr gut, woher der Gedanke kommt, dass es gut ist, Ausnahmen zu unterdrücken, aber er ist einfach nicht mehr anwendbar.

Insbesondere in Ihrem speziellen Fall, in dem Sie sogar die Option angeben, ob Ausnahmen ausgelöst werden sollen oder nicht: Dies bedeutet, dass der Anrufer auf jeden Fall entscheiden muss . Es gibt also überhaupt keinen Nachteil, wenn der Anrufer die Ausnahme abfängt und entsprechend behandelt.

Ein Punkt in einem Kommentar:

In anderen Antworten wurde darauf hingewiesen, dass ein schnelles und hartes Versagen in einer kritischen Anwendung nicht wünschenswert ist, wenn die Hardware, auf der Sie ausgeführt werden, ein Flugzeug ist.

Ein schneller und schwerer Ausfall bedeutet nicht, dass Ihre gesamte Anwendung abstürzt. Dies bedeutet, dass an dem Punkt, an dem der Fehler auftritt, ein lokaler Fehler auftritt. Im Beispiel des OP sollte die Low-Level-Methode, mit der eine bestimmte Fläche berechnet wird, einen Fehler nicht unbemerkt durch einen falschen Wert ersetzen. Es sollte eindeutig scheitern.

Ein Aufrufer der Kette muss diesen Fehler / diese Ausnahme offensichtlich abfangen und entsprechend behandeln. Wenn diese Methode in einem Flugzeug verwendet wurde, sollte dies wahrscheinlich dazu führen, dass eine Fehler-LED aufleuchtet oder zumindest "Fehlerberechnungsbereich" anstelle eines falschen Bereichs angezeigt wird.


3
In anderen Antworten wurde darauf hingewiesen, dass ein schnelles und hartes Versagen in einer kritischen Anwendung nicht wünschenswert ist, wenn die Hardware, auf der Sie ausgeführt werden, ein Flugzeug ist.
HAEM

1
@HAEM, dann ist das ein Missverständnis darüber, was es bedeutet, schnell und hart zu scheitern. Ich habe der Antwort einen Absatz dazu hinzugefügt.
AnoE

Auch wenn die Absicht nicht "so schwer" zu scheitern ist, wird es immer noch als riskant angesehen, mit dieser Art von Feuer zu spielen.
Drjpizzle

Das ist genau der Punkt, @drjpizzle. Wenn Sie es gewohnt sind, hart zu scheitern, dann ist es meiner Erfahrung nach nicht "riskant" oder "mit dieser Art von Feuer zu spielen". Im Gegenteil. Es bedeutet, dass Sie sich daran gewöhnen, darüber nachzudenken, "was passieren würde, wenn ich hier eine Ausnahme erleiden würde", wobei "hier" bedeutet überall , und Sie sind sich in der Regel darüber im Klaren, ob der Ort, an dem Sie gerade programmieren, ein schwerwiegendes Problem haben wird ( Flugzeugabsturz, egal in welchem ​​Fall). Wenn man mit dem Feuer spielt, muss man damit rechnen, dass alles größtenteils in Ordnung ist und jede Komponente im Zweifel so
tut
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.