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.
Antworten:
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.
quelle
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
Es können mehrere Instanzen der Klasse mit unterschiedlichen Konfigurationen im selben Programm zur selben Zeit schweben.
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.
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).
quelle
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
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
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.
quelle
try
/catch
in einer Schleife, wenn die Verarbeitung fortgesetzt werden muss, wenn ein Element ausfällt.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.
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 kleiner als Null ist, ist nicht falsch, wennshouldThrowExceptions
falsch 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?Der Anrufer legt fest, wie fortgefahren werden soll. In diesem Fall erfolgt dies vorzeitig durch Setzen oder Löschen,
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, 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-1
auslö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.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. ;-)
quelle
our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignored
Denkweise 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 wiebool 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.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ß".
quelle
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,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 wurde
area_calc .CalculateArea(x, y)
ein Rich-Datentyp zurückgegeben, der einen Fehler enthielt. Ist es legal, das mit zu multiplizierenz
? 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.vs
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
volume
handelt es sich um einen Rich-Datentyp. Es ist nicht nur eine Zahl. Dies vergrößert den Speicher undvolume
muss noch auf Fehler untersucht werden. Darüber hinaus kannvolume
es 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
volume
es 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.gegen
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.
So etwas wie:
quelle
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:
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 # dasTry-Parse
Muster nennt (danke Peter für den Tipp!)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 reduziertfalse
- "etwas ist schief gelaufen, frag mich nicht was oder wo." (Einiger Dual - APIs verwenden , um einen aussagekräftigen Fehlertyp, wie zum Beispielint errno
oder C ++ 'sstd::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!
Dies ist im Wesentlichen das, was Ihr Mitarbeiter tut; außer dass er den "error handler" in eine globale Variable ausklammert:
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:
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:
Ein Beispiel für etwas, das auf diese Weise funktioniert, ist das
assert
Makro in C und C ++, das sein Verhalten auf dem Präprozessor-Makro festlegtNDEBUG
.quelle
std::optional
von zurückgibtTryCalculateArea()
, ist es einfach, die Implementierung beider Teile der dualen Schnittstelle in einer einzigen Funktionsvorlage mit einem Kompilierungszeit-Flag zu vereinheitlichen.std::expected
. Mit nurstd::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 abflachtfalse
- " etwas ist schief gelaufen, frag mich nicht was oder wo. "std::error_code *ec
gesamten Weg durch alle Ebenen der API und im unteren Bereich das moralische Äquivalent vonif (ec == nullptr) throw something; else *ec = some error code
. (Es abstrahiert das Tatsächlicheif
in etwasErrorHandler
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 alleint
Werte gültige Ergebnisse fürMath.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
out
Schlü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).
quelle
out
Schlüsselwort schreiben Sie einebool
Funktion, die auf das Ergebnis zeigt, dh einenref
Parameter. Sie können dies in VB6 im Jahr 1998 tun. Dasout
Schlü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.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:
Ü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
!shouldThrowExceptions
Modus 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.
quelle
Dieses spezielle Beispiel hat eine interessante Funktion, die sich auf die Regeln auswirken kann ...
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 -
x
undy
vermutlich 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.
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.
quelle
Methoden behandeln entweder Ausnahmen oder nicht, in Sprachen wie C # sind keine Flags erforderlich.
Wenn in ... Code etwas schief geht, muss diese Ausnahme vom Aufrufer behandelt werden. Wenn niemand den Fehler behandelt, wird das Programm beendet.
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.quelle
Dieser Ansatz bricht die Philosophie "schnell scheitern, hart scheitern".
Warum Sie schnell scheitern wollen :
Nachteile, wenn man nicht schnell und hart versagt:
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:
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.
quelle