it-swarm.com.de

Überlegungen zur Fehlerbehandlung

Das Problem :

Seit langer Zeit mache ich mir Sorgen um den Mechanismus exceptions, weil ich der Meinung bin, dass er nicht wirklich löst, was er sollte.

ANSPRUCH: Es gibt lange Debatten außerhalb dieses Themas, und die meisten von ihnen haben Schwierigkeiten, exceptions mit der Rückgabe eines Fehlercodes zu vergleichen. Dies ist hier definitiv nicht das Thema.

Beim Versuch, einen Fehler zu definieren, stimme ich CppCoreGuidelines von Bjarne Stroustrup & Herb Sutter zu

Ein Fehler bedeutet, dass die Funktion ihren angegebenen Zweck nicht erreichen kann

ANSPRUCH: Der Mechanismus exception ist eine Sprachsemantik zur Behandlung von Fehlern.

ANSPRUCH: Für mich gibt es "keine Entschuldigung" für eine Funktion, eine Aufgabe nicht zu erfüllen: Entweder haben wir die Vor-/Nachbedingungen falsch definiert, sodass die Funktion keine Ergebnisse erzielen kann, oder ein bestimmter Ausnahmefall wird nicht als wichtig genug angesehen, um Zeit für die Entwicklung zu verwenden eine Lösung. In Anbetracht dessen, IMO, ist der Unterschied zwischen normaler Code- und Fehlercode-Behandlung (vor der Implementierung) eine sehr subjektive Linie.

ANSPRUCH: Die Verwendung von Ausnahmen, um anzuzeigen, wenn eine Vor- oder Nachbedingung nicht eingehalten wird, ist ein weiterer Zweck des exception -Mechanismus, hauptsächlich zum Debuggen. Ich ziele hier nicht auf diese Verwendung von exceptions ab.

In vielen Büchern, Tutorials und anderen Quellen zeigen sie die Fehlerbehandlung als eine recht objektive Wissenschaft, die mit exceptions gelöst wird, und Sie müssen sie nur catch verwenden, um eine robuste Software zu haben, die in der Lage ist sich von jeder Situation erholen. Aber meine mehrjährige Erfahrung als Entwickler lässt mich das Problem aus einem anderen Blickwinkel betrachten:

  • Programmierer neigen dazu, ihre Aufgabe zu vereinfachen, indem sie Ausnahmen auslösen, wenn der spezifische Fall zu selten erscheint, um sorgfältig implementiert zu werden. Typische Fälle hierfür sind: Probleme mit zu wenig Speicher, Probleme mit dem vollen Datenträger, Probleme mit beschädigten Dateien usw. Dies ist möglicherweise ausreichend, wird jedoch nicht immer auf architektonischer Ebene entschieden.
  • Programmierer neigen dazu, die Dokumentation zu Ausnahmen in Bibliotheken nicht sorgfältig zu lesen, und wissen normalerweise nicht, welche und wann eine Funktion ausgelöst wird. Selbst wenn sie es wissen, verwalten sie sie nicht wirklich.
  • Programmierer neigen dazu, Ausnahmen nicht früh genug zu erkennen, und wenn sie dies tun, ist es meistens, sich zu protokollieren und weiter zu werfen. (siehe ersten Punkt).

Dies hat zwei Konsequenzen:

  1. Häufig auftretende Fehler werden früh in der Entwicklung erkannt und debuggt (was gut ist).
  2. Seltene Ausnahmen werden nicht verwaltet und führen dazu, dass das System beim Benutzer zu Hause abstürzt (mit einer Nice-Protokollnachricht). Manchmal wird der Fehler gemeldet oder nicht einmal.

In Anbetracht dessen sollte IMO der Hauptzweck eines Fehlermechanismus sein:

  1. In Code sichtbar machen, in dem ein bestimmter Fall nicht verwaltet wird.
  2. Kommunizieren Sie die Problemlaufzeit in diesem Fall mit dem zugehörigen Code (zumindest dem Anrufer).
  3. Bietet Wiederherstellungsmechanismen

Der Hauptfehler der exception -Semantik als Fehlerbehandlungsmechanismus ist IMO: Es ist leicht zu erkennen, wo sich ein throw im Quellcode befindet, aber es ist absolut nicht ersichtlich, ob eine bestimmte Funktion dies könnte werfen, indem Sie auf die Erklärung schauen. Dies bringt all das Problem mit sich, das ich oben vorgestellt habe.

Die Sprache erzwingt und überprüft den Fehlercode nicht so streng wie für andere Aspekte der Sprache (z. B. starke Variablentypen).

Ein Lösungsversuch

Um dies zu verbessern, habe ich ein sehr einfaches Fehlerbehandlungssystem entwickelt, das versucht, die Fehlerbehandlung auf die gleiche Wichtigkeit wie den normalen Code zu bringen.

Die Idee ist:

  • Jede (relevante) Funktion erhält einen Verweis auf ein success sehr leichtes Objekt und kann es für den Fall auf einen Fehlerstatus setzen. Das Objekt ist sehr leicht, bis ein Fehler mit Text gespeichert wird.
  • Eine Funktion wird aufgefordert, ihre Aufgabe zu überspringen, wenn das bereitgestellte Objekt bereits einen Fehler enthält.
  • Ein Fehler darf niemals überschrieben werden.

Das vollständige Design berücksichtigt offensichtlich jeden Aspekt (ca. 10 Seiten) und auch die Anwendung auf OOP.

Beispiel für die Klasse Success:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

Verwendungszweck:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

Ich habe das in vielen meiner (eigenen) Codes verwendet und es zwingt den Programmierer (mich), weiter über mögliche Ausnahmefälle nachzudenken und wie man sie löst (gut). Es hat jedoch eine Lernkurve und lässt sich nicht gut in Code integrieren, der es jetzt verwendet.

Die Frage

Ich möchte die Auswirkungen der Verwendung eines solchen Paradigmas in einem Projekt besser verstehen:

  • Ist die Prämisse für das Problem richtig? oder habe ich etwas relevantes verpasst?
  • Ist die Lösung eine gute architektonische Idee? oder ist der preis zu hoch?

BEARBEITEN:

Methodenvergleich:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.
31
Adrian Maire

Die Fehlerbehandlung ist möglicherweise der schwierigste Teil eines Programms.

Im Allgemeinen ist es einfach zu erkennen, dass ein Fehler vorliegt. Es ist jedoch sehr schwierig, es auf eine Weise zu signalisieren, die nicht umgangen werden kann, und es angemessen zu behandeln (siehe Abrahams 'Ausnahmesicherheitsstufen ).

In C werden Signalisierungsfehler durch einen Rückkehrcode verursacht, der für Ihre Lösung isomorph ist.

C++ führte Ausnahmen wegen des Mangels eines solchen Ansatzes ein; Es funktioniert nämlich nur, wenn Anrufer daran denken, zu überprüfen, ob ein Fehler aufgetreten ist oder nicht, und ansonsten schrecklich auseinanderfallen. Immer wenn Sie sagen: "Es ist in Ordnung, solange jedes Mal ...", haben Sie ein Problem. Menschen sind nicht so akribisch, selbst wenn sie sich darum kümmern.

Das Problem ist jedoch, dass Ausnahmen ihre eigenen Probleme haben. Unsichtbarer/versteckter Kontrollfluss. Dies war beabsichtigt: Ausblenden des Fehlerfalls, damit die Logik des Codes nicht durch das Fehlerbehandlungs-Boilerplate verschleiert wird. Es macht den "glücklichen Weg" viel klarer (und schneller!) Auf Kosten der nahezu unergründlichen Fehlerpfade.


Ich finde es interessant zu sehen, wie andere Sprachen das Problem angehen:

  • Java hat Ausnahmen überprüft (und nicht aktiviert),
  • Go verwendet Fehlercodes/Panik,
  • Rust verwendet Summentypen /panics).
  • FP-Sprachen im Allgemeinen.

In C++ gab es früher eine Form von geprüften Ausnahmen. Möglicherweise haben Sie bemerkt, dass diese veraltet und in Richtung einer grundlegenden noexcept(<bool>) vereinfacht wurden: Entweder wird eine Funktion als möglicherweise auslösend deklariert oder als niemals deklariert. Überprüfte Ausnahmen sind insofern etwas problematisch, als sie nicht erweiterbar sind, was zu umständlichen Zuordnungen/Verschachtelungen führen kann. Und verschlungene Ausnahmehierarchien (einer der Hauptanwendungsfälle der virtuellen Vererbung sind Ausnahmen ...).

Im Gegensatz dazu gehen Go und Rust) wie folgt vor:

  • fehler sollten im Band signalisiert werden,
  • ausnahme sollte für wirklich außergewöhnliche Situationen verwendet werden.

Letzteres ist ziemlich offensichtlich darin, dass (1) sie ihre Ausnahmen benennen Panik und (2) es hier keine Typhierarchie/komplizierte Klausel gibt. Die Sprache bietet keine Möglichkeit, den Inhalt einer "Panik" zu überprüfen: keine Typhierarchie, kein benutzerdefinierter Inhalt, nur ein "Ups, die Dinge sind so schief gelaufen, dass keine Wiederherstellung möglich ist".

Dies ermutigt die Benutzer effektiv dazu, die richtige Fehlerbehandlung zu verwenden, und lässt dennoch eine einfache Möglichkeit, in Ausnahmesituationen auszusteigen (z. B. "Warten Sie, das habe ich noch nicht implementiert!").

Natürlich ähnelt der Go-Ansatz leider Ihrem, da Sie leicht vergessen können, den Fehler zu überprüfen ...

... der Ansatz Rust) konzentriert sich jedoch hauptsächlich auf zwei Typen:

  • Option, ähnlich wie std::optional,
  • Result , eine Variante mit zwei Möglichkeiten: Ok und Err.

dies ist viel übersichtlicher, da es keine Möglichkeit gibt, ein Ergebnis versehentlich zu verwenden, ohne auf Erfolg geprüft zu haben: Wenn Sie dies tun, gerät das Programm in Panik.


FP-Sprachen bilden ihre Fehlerbehandlung in Konstrukten, die in drei Ebenen aufgeteilt werden können: - Funktor - Anwendbar/Alternative - Monaden/Alternative

Werfen wir einen Blick auf Haskells Typklasse Functor:

class Functor m where
  fmap :: (a -> b) -> m a -> m b

Erstens sind Typklassen etwas ähnlich, aber nicht gleich Schnittstellen. Haskells Funktionssignaturen sehen auf den ersten Blick etwas beängstigend aus. Aber lasst sie uns entziffern. Die Funktion fmap verwendet eine Funktion als ersten Parameter, die std::function<a,b> Etwas ähnelt. Das nächste ist ein m a. Sie können sich m als etwas wie std::vector Und m a Als etwas wie std::vector<a> Vorstellen. Der Unterschied ist jedoch, dass m a Nicht sagt, dass es explizit std:vector Sein muss. Es könnte also auch ein std::option Sein. Indem wir der Sprache mitteilen, dass wir eine Instanz für die Typklasse Functor für einen bestimmten Typ wie std::vector Oder std::option Haben, können wir die Funktion fmap für diesen Typ verwenden. Dasselbe muss für die Typklassen Applicative, Alternative und Monad durchgeführt werden, mit denen Sie zustandsbehaftete, möglicherweise fehlgeschlagene Berechnungen durchführen können. Die Typklasse Alternative implementiert Abstraktionen zur Fehlerbehebung. Auf diese Weise können Sie etwas wie a <|> b Sagen, was bedeutet, dass es entweder der Begriff a oder der Begriff b ist. Wenn keine der beiden Berechnungen erfolgreich ist, liegt immer noch ein Fehler vor.

Werfen wir einen Blick auf Haskells Typ Maybe.

data Maybe a
  = Nothing
  | Just a

Dies bedeutet, dass Sie dort, wo Sie einen Maybe a Erwarten, entweder Nothing oder Just a Erhalten. Wenn Sie fmap von oben betrachten, könnte eine Implementierung so aussehen

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

Der Ausdruck case ... of Wird als Mustervergleich bezeichnet und ähnelt dem, was in der Welt OOP als visitor pattern Bekannt ist). Stellen Sie sich die Zeile case m of Als m.apply(...) und die Punkte sind die Instanziierung einer Klasse, die die Versandfunktionen implementiert. Die Zeilen unter dem Ausdruck case ... of sind die jeweiligen Versandfunktionen, die die Felder der Klasse direkt nach Namen in den Gültigkeitsbereich bringen. In Nothing Zweig erstellen wir Nothing und im Zweig Just a benennen wir unseren einzigen Wert a und erstellen einen weiteren Just ... mit der Transformationsfunktion f, die auf a angewendet wird. Lesen Sie ihn als: new Just(f(a)).

Dies kann jetzt fehlerhafte Berechnungen verarbeiten, während die eigentlichen Fehlerprüfungen abstrahiert werden. Es gibt Implementierungen für die anderen Schnittstellen, was diese Art von Berechnungen sehr leistungsfähig macht. Tatsächlich ist Maybe die Inspiration für Rusts Option- Type.


Ich würde Sie dort ermutigen, stattdessen Ihre Klasse Success in Richtung Result zu überarbeiten. Alexandrescu schlug tatsächlich etwas sehr Nahes vor, genannt expected<T>, Für das Standardvorschläge gemacht wurden .

Ich werde mich an die Rust Benennung und API halten, einfach weil ... sie dokumentiert ist und funktioniert. Natürlich Rust hat einen raffinierten ? Suffix-Operator, der den Code viel süßer machen würde; in C++ verwenden wir das Makro TRY und GCCs Anweisungsausdruck , um ihn zu emulieren.

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

Hinweis: Dieser Result ist ein Platzhalter. Eine ordnungsgemäße Implementierung würde eine Kapselung und einen union verwenden. Es reicht jedoch aus, den Punkt zu vermitteln.

Was mir erlaubt zu schreiben ( siehe es in Aktion ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

was ich wirklich ordentlich finde:

  • im Gegensatz zur Verwendung von Fehlercodes (oder Ihrer Klasse Success) führt das Vergessen, nach Fehlern zu suchen, zu einem Laufzeitfehler1 eher als irgendein zufälliges Verhalten,
  • im Gegensatz zur Verwendung von Ausnahmen ist es offensichtlich, dass an der Aufrufstelle welche Funktionen fehlschlagen können, sodass es keine Überraschung gibt.
  • mit dem C++ - 2X-Standard erhalten wir möglicherweise concepts im Standard. Dies würde diese Art der Programmierung weitaus angenehmer machen, da wir die Wahl der Fehlerart überlassen könnten. Z.B. Mit einer Implementierung von std::vector als Ergebnis könnten wir alle möglichen Lösungen auf einmal berechnen. Oder wir könnten uns dafür entscheiden, die Fehlerbehandlung zu verbessern, wie Sie vorgeschlagen haben.

1  Mit einer ordnungsgemäß gekapselten Implementierung von Result;)


Hinweis: Im Gegensatz zu Ausnahmen verfügt dieser leichtgewichtige Result nicht über Backtraces, wodurch die Protokollierung weniger effizient ist. Möglicherweise ist es hilfreich, mindestens die Datei-/Zeilennummer zu protokollieren, unter der die Fehlermeldung generiert wird, und im Allgemeinen eine umfangreiche Fehlermeldung zu schreiben. Dies kann noch verstärkt werden, indem die Datei/Zeile jedes Mal erfasst wird, wenn das Makro TRY verwendet wird, die Rückverfolgung im Wesentlichen manuell erstellt wird oder plattformspezifischer Code und Bibliotheken wie libbacktrace verwendet werden, um die Symbole im Callstack aufzulisten.


Es gibt jedoch eine große Einschränkung: Bestehende C++ - Bibliotheken und sogar std basieren auf Ausnahmen. Es wird ein harter Kampf sein, diesen Stil zu verwenden, da die API einer Drittanbieter-Bibliothek in einen Adapter eingeschlossen werden muss ...

32
Matthieu M.

CLAIM: Der Ausnahmemechanismus ist eine Sprachsemantik zur Behandlung von Fehlern

ausnahmen sind ein Kontrollflussmechanismus. Die Motivation für diesen Kontrollflussmechanismus bestand darin, die Fehlerbehandlung vom Nicht-Fehlerbehandlungscode zu trennen, in dem allgemeinen Fall, dass die Fehlerbehandlung sehr repetitiv ist und hat wenig Relevanz für den Hauptteil der Logik.

ANSPRUCH: Für mich gibt es "keine Entschuldigung" für eine Funktion, eine Aufgabe nicht zu erfüllen: Entweder haben wir die Vor-/Nachbedingungen falsch definiert, sodass die Funktion keine Ergebnisse erzielen kann, oder ein bestimmter Ausnahmefall wird nicht als wichtig genug angesehen, um Zeit für die Entwicklung zu verwenden eine Lösung

Bedenken Sie: Ich versuche, eine Datei zu erstellen. Das Speichergerät ist voll.

Dies ist kein Fehler bei der Definition meiner Voraussetzungen: Sie können "Es muss genügend Speicher vorhanden sein" im Allgemeinen nicht als Voraussetzung verwenden, da der gemeinsam genutzte Speicher den Rennbedingungen unterliegt, die dies unmöglich machen.

Sollte mein Programm also irgendwie Speicherplatz freigeben und dann erfolgreich fortfahren, sonst bin ich einfach zu faul, um "eine Lösung zu entwickeln"? Dies scheint offen gesagt unsinnig. Die "Lösung" für die Verwaltung des gemeinsam genutzten Speichers liegt außerhalb des Bereichs meines Programms und ermöglicht, dass mein Programm ordnungsgemäß fehlschlägt und erneut ausgeführt wird, sobald der Benutzer es tut hat entweder etwas Speicherplatz freigegeben oder etwas mehr Speicher hinzugefügt, ist in Ordnung .


Ihre Erfolgsklasse verschachtelt die Fehlerbehandlung sehr explizit mit Ihrer Programmlogik. Jede einzelne Funktion muss vor dem Ausführen überprüfen, ob bereits ein Fehler aufgetreten ist, was bedeutet, dass sie nichts tun sollte. Jede Bibliotheksfunktion muss in eine andere Funktion mit einem weiteren Argument (und hoffentlich perfekter Weiterleitung) eingeschlossen werden, die genau dasselbe tut.

Beachten Sie auch, dass Ihre mySqrt -Funktion einen Wert zurückgeben muss, auch wenn , wenn sie fehlgeschlagen ist (oder eine vorherige Funktion fehlgeschlagen ist). Sie geben also entweder einen magischen Wert zurück (wie NaN) oder fügen einen unbestimmten Wert in Ihr Programm ein und hoffen , dass nichts verwendet wird das, ohne den Erfolgsstatus zu überprüfen, den Sie durch Ihre Ausführung gezogen haben.

Aus Gründen der Korrektheit - und Leistung - ist es viel besser, die Kontrolle wieder außerhalb des Geltungsbereichs zu verlieren, wenn Sie keine Fortschritte erzielen können. Ausnahmen und die explizite Fehlerprüfung im C-Stil mit vorzeitiger Rückgabe erreichen dies beide.


Zum Vergleich: Ein Beispiel für Ihre Idee, die wirklich funktioniert, ist Fehlermonade in Haskell. Der Vorteil gegenüber Ihrem System besteht darin, dass Sie den Großteil Ihrer Logik normal schreiben und dann in die Monade einschließen, die dafür sorgt, dass die Auswertung angehalten wird, wenn ein Schritt fehlschlägt. Auf diese Weise ist der einzige Code, der das Fehlerbehandlungssystem direkt berührt, der Code, der möglicherweise fehlschlägt (einen Fehler auslösen), und der Code, der den Fehler bewältigen muss (eine Ausnahme abfangen).

Ich bin mir nicht sicher, ob Monadenstil und faule Auswertung sich gut in C++ übersetzen lassen.

46
Useless

Ich möchte die Auswirkungen der Verwendung eines solchen Paradigmas in einem Projekt besser verstehen:

  • Ist die Prämisse für das Problem richtig? oder habe ich etwas relevantes verpasst?
  • Ist die Lösung eine gute architektonische Idee? oder ist der preis zu hoch?

Ihr Ansatz bringt einige große Probleme in Ihren Quellcode:

  • es basiert auf dem Client-Code, der immer daran denkt, den Wert von s zu überprüfen. Dies ist üblich mit dem Ansatz verwenden Sie Rückkehrcodes für die Fehlerbehandlung und einem der Gründe, warum Ausnahmen in die Sprache eingeführt wurden: Mit Ausnahmen schlagen Sie nicht stillschweigend fehl, wenn Sie fehlschlagen.

  • je mehr Code Sie mit diesem Ansatz schreiben, desto mehr Fehler müssen Sie auch für die Fehlerbehandlung hinzufügen (Ihr Code ist nicht mehr minimalistisch), und Ihr Wartungsaufwand steigt.

Aber meine mehrjährige Erfahrung als Entwickler lässt mich das Problem aus einem anderen Blickwinkel betrachten:

Die Lösungen für diese Probleme sollten auf technischer Leitungsebene oder auf Teamebene angegangen werden:

Programmierer neigen dazu, ihre Aufgabe zu vereinfachen, indem sie Ausnahmen auslösen, wenn der spezifische Fall zu selten erscheint, um sorgfältig implementiert zu werden. Typische Fälle hierfür sind: Probleme mit zu wenig Speicher, Probleme mit dem vollen Datenträger, Probleme mit beschädigten Dateien usw. Dies ist möglicherweise ausreichend, wird jedoch nicht immer auf architektonischer Ebene entschieden.

Wenn Sie ständig mit jeder Art von Ausnahme umgehen, die möglicherweise ausgelöst wird, ist das Design nicht gut. Welche Fehler behandelt werden, sollte anhand der Projektspezifikationen entschieden werden, nicht anhand der Meinung der Entwickler zur Implementierung.

Adressieren Sie dies, indem Sie automatisierte Tests einrichten, die Spezifikation der Komponententests und die Implementierung trennen (lassen Sie dies von zwei verschiedenen Personen durchführen).

Programmierer neigen dazu, die Dokumentation [...] nicht sorgfältig zu lesen. Selbst wenn sie es wissen, verwalten sie sie nicht wirklich.

Sie werden dies nicht beheben, indem Sie mehr Code schreiben. Ich denke, Ihre beste Wahl sind sorgfältig angewandte Codeüberprüfungen.

Programmierer neigen dazu, Ausnahmen nicht früh genug zu erkennen, und wenn sie dies tun, ist es meistens, sich zu protokollieren und weiter zu werfen. (siehe ersten Punkt).

Die richtige Fehlerbehandlung ist schwierig, aber mit Ausnahmen weniger langwierig als mit Rückgabewerten (unabhängig davon, ob sie tatsächlich zurückgegeben oder als E/A-Argumente übergeben werden).

Der schwierigste Teil der Fehlerbehandlung besteht nicht darin, wie Sie den Fehler erhalten, sondern wie Sie sicherstellen, dass Ihre Anwendung bei Fehlern einen konsistenten Status beibehält.

Um dies zu beheben, muss der Identifizierung und Ausführung unter Fehlerbedingungen (mehr Tests, mehr Einheiten-/Integrationstests usw.) mehr Aufmerksamkeit gewidmet werden.

15
utnapistim