Code Verträge / Behauptungen: Was ist mit doppelten Schecks?

10

Ich bin ein großer Fan von Zusicherungen, Verträgen oder anderen Arten von Schecks, die in der von mir verwendeten Sprache verfügbar sind. Eine Sache, die mich ein bisschen stört, ist, dass ich nicht sicher bin, wie es üblich ist, mit doppelten Prüfungen umzugehen.

Beispielsituation: Ich schreibe zuerst die folgende Funktion

void DoSomething( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  //code using obj
}

dann schreibe ich ein paar Stunden später eine andere Funktion, die die erste aufruft. Da alles noch frisch im Speicher ist, entscheide ich mich, den Vertrag nicht zu duplizieren, da ich weiß, dass DoSomethingbereits nach einem Nullobjekt gesucht wird:

void DoSomethingElse( object obj )
{
  //no Requires here: DoSomething will do that already
  DoSomething( obj );
  //code using obj
}

Das offensichtliche Problem: DoSomethingElsehängt jetzt davon ab DoSomething, ob obj nicht null ist. DoSomethingSollte sich also jemals entscheiden, nicht mehr zu prüfen, oder wenn ich mich entscheide, eine andere Funktion zu verwenden, wird obj möglicherweise nicht mehr geprüft. Was mich schließlich dazu bringt, diese Implementierung zu schreiben:

void DoSomethingElse( object obj )
{
  Contract.Requires<ArgumentNullException>( obj != null );
  DoSomething( obj );
  //code using obj
}

Immer sicher, keine Sorge, außer dass, wenn die Situation wächst, dasselbe Objekt möglicherweise mehrmals überprüft wird und es sich um eine Form der Vervielfältigung handelt, und wir alle wissen, dass dies nicht so gut ist.

Was ist die gängigste Praxis für solche Situationen?

stijn
quelle
3
ArgumentBullException? Das ist neu :)
ein CVn
lol @ meine Schreibfähigkeiten ... Ich werde es bearbeiten.
Stijn

Antworten:

13

Persönlich würde ich in jeder Funktion, die fehlschlägt, wenn sie eine Null erhält, nach Null suchen, und nicht in einer Funktion, die dies nicht tut.

Wenn also in Ihrem obigen Beispiel doSomethingElse () obj nicht dereferenzieren muss, würde ich obj dort nicht auf null prüfen.

Wenn DoSomething () das Objekt dereferenziert, sollte es auf null prüfen.

Wenn beide Funktionen es dereferenzieren, sollten sie beide prüfen. Wenn DoSomethingElse dereferences obj ist, sollte es auf null prüfen, aber DoSomething sollte auch weiterhin auf null prüfen, da es möglicherweise von einem anderen Pfad aufgerufen wird.

Auf diese Weise können Sie den Code ziemlich sauber lassen und trotzdem sicherstellen, dass die Überprüfungen an der richtigen Stelle sind.

Luke Graham
quelle
1
Ich stimme vollkommen zu. Die Voraussetzungen jeder Methode sollten für sich stehen. Stellen Sie sich vor, Sie schreiben neu DoSomething(), sodass die Vorbedingung nicht mehr erforderlich ist (in diesem speziellen Fall unwahrscheinlich, kann aber in einer anderen Situation auftreten), und entfernen Sie die Voraussetzungsprüfung. Jetzt ist eine scheinbar völlig unabhängige Methode wegen der fehlenden Voraussetzung gebrochen. Ich werde ein wenig Code duplizieren, um Klarheit über solche seltsamen Fehler zu schaffen, aus dem Wunsch heraus, jeden Tag ein paar Codezeilen zu speichern.
Ein CVn
2

Groß! Ich sehe, Sie haben von Code Contracts for .NET erfahren. Codeverträge gehen weit über Ihre durchschnittlichen Behauptungen hinaus, wofür der statische Prüfer das beste Beispiel ist. Dies steht Ihnen möglicherweise nicht zur Verfügung, wenn Sie Visual Studio Premium oder höher nicht installiert haben. Es ist jedoch wichtig, die Absichten dahinter zu verstehen, wenn Sie Codeverträge verwenden möchten.

Wenn Sie einen Vertrag auf eine Funktion anwenden, handelt es sich buchstäblich um einen Vertrag . Diese Funktion garantiert ein vertragsgemäßes Verhalten und wird garantiert nur gemäß der vertraglichen Definition verwendet.

In Ihrem Beispiel entspricht die DoSomethingElse()Funktion nicht dem von angegebenen Wert DoSomething(), da null übergeben werden kann und der statische Prüfer dieses Problem anzeigt. Sie können dies beheben, indem Sie denselben Vertrag hinzufügen DoSomethingElse().

Dies bedeutet, dass es zu einer Duplizierung kommt. Diese Duplizierung ist jedoch erforderlich, wenn Sie die Funktionalität für zwei Funktionen verfügbar machen. Obwohl diese Funktionen privat sind, können sie auch von verschiedenen Stellen in Ihrer Klasse aufgerufen werden. Die einzige Möglichkeit, um sicherzustellen, dass das Argument bei einem bestimmten Aufruf niemals null ist, besteht darin, die Verträge zu duplizieren.

Dies sollte Sie überdenken lassen, warum Sie das Verhalten überhaupt in zwei Funktionen aufgeteilt haben. Ich war immer der Meinung ( entgegen der landläufigen Meinung ), dass man Funktionen, die nur von einem Ort aus aufgerufen werden, nicht aufteilen sollte . Durch das Aufdecken der Kapselung durch Anwenden der Verträge wird dies noch deutlicher. Es scheint, ich habe eine zusätzliche Argumentation für meine Sache gefunden! Vielen Dank! :) :)

Steven Jeuris
quelle
zu Ihrem letzten Absatz: Im eigentlichen Code waren beide Funktionen Mitglieder zweier verschiedener Klassen, weshalb sie aufgeteilt sind. Abgesehen davon war ich so oft in der folgenden Situation: Schreiben Sie eine lange Funktion, entscheiden Sie sich, sie nicht zu teilen. Stellen Sie später fest, dass ein Teil der Logik an einer anderen Stelle dupliziert wurde. Teilen Sie sie also trotzdem auf. Oder ein Jahr später lesen Sie es noch einmal und finden es unlesbar, also teilen Sie es trotzdem. Oder beim Debuggen: Split-Funktionen = weniger Druck auf die F10-Taste. Es gibt noch mehr Gründe, deshalb bevorzuge ich persönlich das Teilen, obwohl es manchmal zu extrem sein kann.
Stijn
(1) "Später herausfinden, dass ein Teil der Logik woanders dupliziert wurde" . Deshalb finde ich es wichtiger, sich immer "in Richtung einer API zu entwickeln", als einfach Funktionen aufzuteilen. Denken Sie ständig an die Wiederverwendung, nicht nur innerhalb der aktuellen Klasse. (2) "Oder ein Jahr später noch einmal lesen und für unlesbar halten" Weil Funktionen Namen haben, ist das besser? Sie haben noch mehr Lesbarkeit, wenn Sie einen Kommentar in meinem Blog verwenden , der als "Code-Absätze" bezeichnet wird. (3) "Split-Funktionen = weniger Druck auf die F10-Taste" ... Ich verstehe nicht warum.
Steven Jeuris
(1) vereinbart (2) Lesbarkeit ist eine persönliche Präferenz, daher wird das für mich nicht wirklich diskutiert. (3) Um eine Funktion von 20 Zeilen zu durchlaufen, muss F10 20 Mal gedrückt werden. Wenn ich eine Funktion durchlaufe, die 10 dieser Zeilen in einer geteilten Funktion enthält, muss ich nur 11 Mal F10 drücken. Ja, im ersten Fall kann ich Haltepunkte setzen oder "Zum Cursor springen" auswählen, aber das ist immer noch ein größerer Aufwand als im zweiten Fall.
Stijn
@stijn: (2) stimmte zu; p (3) danke für die Klarstellung!
Steven Jeuris