Voraussetzungen prüfen oder nicht

8

Ich wollte eine solide Antwort auf die Frage finden, ob Laufzeitprüfungen durchgeführt werden sollen, um Eingaben zu validieren, um sicherzustellen, dass ein Kunde das vertraglich festgelegte Ende der Vereinbarung eingehalten hat. Stellen Sie sich zum Beispiel einen einfachen Klassenkonstruktor vor:

class Foo
{
public:
  Foo( BarHandle bar )
  {
    FooHandle handle = GetFooHandle( bar );
    if( handle == NULL ) {
      throw std::exception( "invalid FooHandle" );
    }
  }
};

Ich würde in diesem Fall argumentieren, dass ein Benutzer nicht versuchen sollte, eine Fooohne eine gültige zu erstellen BarHandle. Es scheint nicht richtig zu sein, zu überprüfen, ob dies barinnerhalb des FooKonstruktors gültig ist . Wenn ich einfach dokumentiere, dass Fooder Konstruktor eine gültige erfordert BarHandle, ist das nicht genug? Ist dies ein angemessener Weg, um meine Vertragsvoraussetzungen vertraglich durchzusetzen?

Bisher hat alles, was ich gelesen habe, gemischte Meinungen dazu. Es scheint, als würden 50% der Leute sagen, dass sie überprüfen sollen, ob sie bargültig sind, die anderen 50% würden sagen, dass ich es nicht tun sollte. Betrachten Sie beispielsweise einen Fall, in dem der Benutzer überprüft, ob sie BarHandlekorrekt sind, aber eine zweite (und unnötige) Überprüfung wird auch innerhalb des FooKonstruktors ausgeführt.

void.pointer
quelle

Antworten:

10

Ich glaube nicht, dass es eine einzige Antwort darauf gibt. Ich denke, die Hauptsache, die notwendig ist, ist Konsistenz - entweder erzwingen Sie alle Voraussetzungen für eine Funktion, oder Sie versuchen nicht, eine davon durchzusetzen . Leider ist das ziemlich selten - was normalerweise passiert, ist, dass Programmierer, anstatt über die Voraussetzungen nachzudenken und sie durchzusetzen, Codebits hinzufügen, um Vorbedingungen durchzusetzen, deren Verletzung beim Testen zu Fehlern geführt hat, aber häufig andere Möglichkeiten offen lassen, die jedoch zu Fehlern führen können kam beim Testen nicht vor.

In vielen Fällen ist es durchaus sinnvoll, zwei Ebenen bereitzustellen: eine für die "interne" Verwendung, bei der keine Vorbedingungen erzwungen werden, und eine zweite für die "externe" Verwendung, bei der nur die Voraussetzungen erzwungen werden, und die erste.

Ich denke jedoch, dass es besser ist, die Voraussetzungen im Quellknoten durchzusetzen und nicht nur zu dokumentieren. Eine Ausnahme oder Behauptung ist viel schwieriger zu ignorieren als die Dokumentation und bleibt mit größerer Wahrscheinlichkeit mit dem Rest des Codes synchron.

Jerry Sarg
quelle
Grundsätzlich stimme ich Ihrem letzten Absatz zu. Obwohl dies jetzt bedeutet, dass drei Dinge synchron gehalten werden müssen; die Dokumentation, die Behauptungen selbst und die Testfälle, die beweisen, dass die Behauptungen ihre Arbeit machen (wenn Sie an solche Dinge glauben)!
Oliver Charlesworth
@OliCharlesworth: Ja, es wird eine dritte Sache erstellt, die synchron gehalten werden soll, aber es wird eine (die Durchsetzung im Quellcode) als diejenige festgelegt, der allgemein vertraut wird, wenn es Meinungsverschiedenheiten gibt. Ansonsten wissen Sie es im Allgemeinen nicht.
Jerry Coffin
2
@ JerryCoffin Ich könnte überprüfen, ob fooNULL ist, aber NULL zu sein ist nicht der einzige Weg foo, der ungültig sein könnte. Was ist zum Beispiel mit -1, das in a umgewandelt wird FooHandle? Ich kann nicht alle möglichen Möglichkeiten überprüfen, wie das Handle ungültig sein könnte. NULL ist eine offensichtliche Wahl und etwas, auf das normalerweise geprüft wird, aber keine abschließende Prüfung. Was würden Sie hier empfehlen?
void.pointer
@ RobertDailey: Letztendlich ist es fast unmöglich, sich gegen jeden möglichen Missbrauch abzusichern , besonders wenn / wenn Casting involviert ist. Mit Casting kann der Benutzer im Wesentlichen alles untergraben, was Sie überprüfen können. Was ich am meisten betone, ist der Unterschied zwischen 1) der Annahme, dass die Parameter gut sind, und dem Hinzufügen von Überprüfungen für die Fehler beim Testen und 2) dem Herausfinden der Voraussetzungen so genau wie möglich und dem Erzwingen dieser Voraussetzungen .
Jerry Coffin
@ JerryCoffin Wie unterscheidet sich dies von der defensiven Programmierung, die im Allgemeinen nicht als "gute Sache" angesehen wird? Defensive Programmiertechniken wie diese und viele andere, die ich gesehen habe, sind meistens nicht sehr pragmatisch. Es ist ein Design, das entwickelt wurde, um die schlechten Codierungsgewohnheiten oder andere Dinge Ihrer Mitarbeiter zu bekämpfen, anstatt sich auf die praktische Funktionalität und die Implementierung Ihrer Methoden zu konzentrieren. Ich sehe dies nur als eine Gewohnheit, die zusätzliche Kesselplattenlogik in allen Klassenfunktionen hinzufügt. Sie würden denken, dass Unit-Tests die Notwendigkeit dieser Vorbedingungsprüfungen überflüssig machen.
void.pointer
4

Es ist eine sehr schwierige Frage, da es verschiedene Konzepte gibt:

  • Richtigkeit
  • Dokumentation
  • Performance

Dies ist jedoch meist ein Artefakt einer Art Fehler, in diesem Fall. Die Nullheit wird besser durch Typeinschränkungen erzwungen, da der Compiler diese tatsächlich überprüft. Da jedoch nicht alles in einem Typsystem erfasst werden kann, insbesondere in C ++, lohnt sich die Frage selbst immer noch.


Persönlich denke ich, dass Korrektheit und Dokumentation von größter Bedeutung sind. Schnell und falsch zu sein ist nutzlos. Schnell und nur manchmal falsch zu sein ist ein bisschen besser, bringt aber auch nicht viel auf den Tisch.

Die Leistung kann jedoch in einigen Teilen der Programme kritisch sein, und einige Überprüfungen können sehr umfangreich sein (dh beweisen, dass auf einen gerichteten Graphen alle Knoten sowohl zugänglich als auch gemeinsam zugänglich sind). Daher würde ich für einen doppelten Ansatz stimmen.

Prinzip eins: Schnell scheitern . Dies ist ein Leitprinzip in der defensiven Programmierung im Allgemeinen, das die frühestmögliche Erkennung von Fehlern befürwortet. Ich würde Fail Hard zur Gleichung hinzufügen .

if (not bar) { abort(); }

Leider ist ein hartes Versagen in einer Produktionsumgebung nicht unbedingt die beste Lösung. In diesem Fall kann eine bestimmte Ausnahme dazu beitragen, dass Sie schnell von dort wegkommen und einen hochrangigen Handler den fehlgeschlagenen Fall angemessen erfassen und behandeln können (höchstwahrscheinlich wird ein neuer Fall protokolliert und vorangetrieben).

Dies spricht jedoch nicht das Problem teurer Tests an. An Hot Spots können diese Tests zu viel kosten. In diesem Fall ist es sinnvoll, den Test nur in DEBUG-Builds zu aktivieren.

Dies lässt uns eine schöne und einfache Lösung:

  • SOFT_ASSERT(Cond_, Text_)
  • DEBUG_ASSERT(Cond_, Text_)

Wo die beiden Makros folgendermaßen definiert sind:

 #ifdef NDEBUG
 #  define SOFT_ASSERT(Cond_, Text_)                                                \
        while (not (Cond_)) { throw Exception(Text_, __FILE__, __LINE__); }
 #  define DEBUG_ASSERT(Cond_, Text_) while(false) {}
 #else // NDEBUG
 #  define SOFT_ASSERT(Cond_, Text_)                                                \
        while (not (Cond_)) {                                                       \
            std::cerr << __FILE__ << '#' << __LINE__ << ": " << Text_ << std::endl; \
            abort();                                                                \
        }
 #  define DEBUG_ASSERT(Cond_, Text_) SOFT_ASSERT(Cond_, Text_)
 #endif // NDEBUG
Matthieu M.
quelle
0

Ein Zitat, das ich dazu gehört habe, ist:

"Seien Sie konservativ in dem, was Sie tun, und liberal in dem, was Sie akzeptieren."

Was darauf hinausläuft, den Verträgen für Argumente zu folgen, wenn Sie Funktionen aufrufen, und alle Eingaben zu überprüfen, bevor Sie beim Schreiben von Funktionen handeln.

Letztendlich kommt es auf die Domain an. Wenn Sie eine Betriebssystem-API ausführen, sollten Sie jede Eingabe überprüfen und nicht allen eingehenden Daten als gültig vertrauen, bevor Sie darauf reagieren. Wenn Sie eine Bibliothek für andere Benutzer erstellen, lassen Sie den Benutzer sich selbst verarschen (OpenGL fällt Ihnen aus einem unbekannten Grund zuerst ein).

BEARBEITEN: Im OO-Sinne scheint es zwei Ansätze zu geben - einen, der besagt, dass ein Objekt niemals für die gesamte Zeit, in der auf ein Objekt zugegriffen werden darf, fehlerhaft sein sollte (alle seine Invarianten müssen wahr sein), und einen anderen Ansatz, der besagt, dass Sie einen Konstruktor haben Wenn nicht alle Invarianten festgelegt sind, legen Sie einige weitere Werte fest und haben eine zweite Initialisierungsfunktion, die init beendet.

Ich mag das erstere irgendwie besser, da es kein magisches Wissen erfordert oder sich auf die aktuelle Dokumentation stützt, um zu wissen, welche Teile der Initialisierung der Konstruktor nicht tut.

anon
quelle
Für eine solche Initialisierung bevorzuge ich ein Builder-Objekt, das Teilinitialisierungsdaten enthält und später ein vollständig inoffizielles "nützliches" Objekt erstellt.
user470365