Vergleichen Sie redundante Zustandsüberprüfungen mit Best Practices?

16

Ich habe in den letzten drei Jahren Software entwickelt, aber erst kürzlich wurde mir klar, wie ignorant ich in Bezug auf gute Praktiken bin. Dies hat mich veranlasst, das Buch Clean Code zu lesen , das mein Leben positiv beeinflusst, aber ich kämpfe darum, einen Einblick in einige der besten Ansätze zum Schreiben meiner Programme zu bekommen.

Ich habe ein Python-Programm, in dem ich ...

  1. Verwenden Sie argparse required=True, um zwei Argumente zu erzwingen, die beide Dateinamen sind. Der erste ist der Name der Eingabedatei, der zweite der Name der Ausgabedatei
  2. haben eine Funktion, readFromInputFiledie zuerst prüft, ob ein Eingabedateiname eingegeben wurde
  3. haben eine Funktion, writeToOutputFiledie zuerst prüft, ob ein Ausgabedateiname eingegeben wurde

Mein Programm ist klein genug, um zu glauben, dass das Einchecken von Nr. 2 und Nr. 3 überflüssig ist und entfernt werden sollte, wodurch beide Funktionen von einer unnötigen ifBedingung befreit werden. Ich bin jedoch auch zu der Überzeugung gelangt, dass "Doppelprüfungen in Ordnung sind" und möglicherweise die richtige Lösung in einem Programm sind, in dem die Funktionen von einem anderen Ort aus aufgerufen werden können, an dem das Parsen von Argumenten nicht erfolgt.

(Auch wenn das Lesen oder Schreiben fehlschlägt, muss ich try exceptin jeder Funktion eine entsprechende Fehlermeldung auslösen.)

Meine Frage ist: Ist es am besten, alle redundanten Zustandsüberprüfungen zu vermeiden? Sollte die Logik eines Programms so solide sein, dass Überprüfungen nur einmal durchgeführt werden müssen? Gibt es gute Beispiele, die dies oder das Gegenteil veranschaulichen?

EDIT: Vielen Dank für die Antworten! Ich habe von jedem etwas gelernt. Wenn ich so viele Perspektiven sehe, verstehe ich viel besser, wie ich dieses Problem angehen und anhand meiner Anforderungen eine Lösung finden kann. Vielen Dank!

These
quelle
Hier ist eine stark verallgemeinerte Version Ihrer Frage: softwareengineering.stackexchange.com/questions/19549/… . Ich würde nicht sagen, dass es doppelt ist, da es einen viel größeren Fokus hat, aber vielleicht hilft es.
Doc Brown

Antworten:

15

Was Sie verlangen, heißt "Robustheit", und es gibt keine richtige oder falsche Antwort. Dies hängt von der Größe und Komplexität des Programms, der Anzahl der Mitarbeiter und der Bedeutung der Fehlererkennung ab.

In kleinen Programmen, die Sie alleine und nur für sich selbst schreiben, ist Robustheit in der Regel ein viel geringeres Problem als wenn Sie ein komplexes Programm schreiben, das aus mehreren Komponenten besteht, die möglicherweise von einem Team geschrieben wurden. In solchen Systemen gibt es Grenzen zwischen den Komponenten in Form von öffentlichen APIs, und an jeder Grenze ist es oft eine gute Idee, die Eingabeparameter zu validieren, selbst wenn "die Logik des Programms so solide sein sollte, dass diese Prüfungen redundant sind ". Das erleichtert das Erkennen von Fehlern und hilft, die Debugging-Zeiten zu verkürzen.

In Ihrem Fall müssen Sie selbst entscheiden, welche Art von Lebenszyklus Sie für Ihr Programm erwarten. Ist es ein Programm, von dem Sie erwarten, dass es über Jahre hinweg verwendet und gewartet wird? Dann ist es wahrscheinlich besser, eine redundante Prüfung hinzuzufügen, da es nicht unwahrscheinlich ist, dass Ihr Code in Zukunft überarbeitet wird und Ihre readund write-Funktionen in einem anderen Kontext verwendet werden.

Oder ist es ein kleines Programm nur zum Lernen oder zum Spaß? Dann sind diese doppelten Überprüfungen nicht notwendig.

Im Zusammenhang mit "Clean Code" könnte man fragen, ob eine doppelte Überprüfung gegen das DRY-Prinzip verstößt. Tatsächlich ist dies manchmal zumindest in geringem Maße der Fall: Die Eingabevalidierung kann als Teil der Geschäftslogik eines Programms interpretiert werden, und dies an zwei Stellen zu tun, kann zu den üblichen Wartungsproblemen führen, die durch die Verletzung von DRY verursacht werden. Robustheit gegenüber DRY ist oft ein Kompromiss - Robustheit erfordert Redundanz im Code, während DRY versucht, Redundanz zu minimieren. Und mit zunehmender Programmkomplexität wird Robustheit immer wichtiger, als bei der Validierung trocken zu sein.

Lassen Sie mich zum Schluss ein Beispiel geben, was das für Sie bedeutet. Nehmen wir an, Ihre Anforderungen ändern sich in etwas wie

  • Das Programm soll auch mit einem Argument arbeiten, dem Eingabedateinamen. Wenn kein Ausgabedateiname angegeben ist, wird dieser automatisch aus dem Eingabedateinamen erstellt, indem das Suffix ersetzt wird.

Ist es deshalb wahrscheinlich, dass Sie Ihre Doppelvalidierung an zwei Stellen ändern müssen? Wahrscheinlich nicht, eine solche Anforderung führt zu einer Änderung beim Aufrufen argparse, aber zu keiner Änderung writeToOutputFile: Diese Funktion erfordert weiterhin einen Dateinamen. In Ihrem Fall würde ich also für die zweimalige Eingabeüberprüfung stimmen. Das Risiko von Wartungsproblemen aufgrund von zwei zu ändernden Stellen ist meiner Meinung nach viel geringer als das Risiko von Wartungsproblemen aufgrund maskierter Fehler, die durch zu wenige Überprüfungen verursacht werden.

Doc Brown
quelle
"... Grenzen zwischen Komponenten in Form von öffentlichen APIs ..." Ich beobachte, dass "Klassen sozusagen Grenzen springen". Was also gebraucht wird, ist eine Klasse; eine zusammenhängende Business-Domain-Klasse. Ich schließe aus diesem OP, dass das allgegenwärtige Prinzip "Es ist einfach, also brauche keine Klasse" hier am Werk ist. Es könnte eine einfache Klasse geben, die das "primäre Objekt" umschließt und Geschäftsregeln wie "Eine Datei muss einen Namen haben" erzwingt, die den vorhandenen Code nicht nur DRY-fähig machen, sondern ihn auch in Zukunft DRY-fähig halten.
Radarbob
@radarbob: was ich geschrieben habe, ist nicht auf OOP oder Komponenten in Form von Klassen beschränkt. Dies gilt auch für beliebige Bibliotheken mit einer öffentlichen API, ob objektorientiert oder nicht.
Doc Brown
5

Redundanz ist nicht die Sünde. Überflüssige Redundanz ist.

  1. Wenn readFromInputFile()und writeToOutputFile()eine öffentliche Funktion sind (und gemäß Python-Namenskonventionen, da ihre Namen nicht mit zwei Unterstrichen beginnen), könnten die Funktionen eines Tages von jemandem verwendet werden, der eine Auseinandersetzung vollständig vermieden hat. Das heißt, wenn sie die Argumente weglassen, wird Ihre benutzerdefinierte Fehlermeldung argparse nicht angezeigt.

  2. Wenn readFromInputFile()und writeToOutputFile()sich für die Parameter überprüfen, erhalten Sie erneut eine benutzerdefinierte Fehlermeldung zu zeigen , dass die Notwendigkeit für Dateinamen erklärt.

  3. Wenn readFromInputFile()und writeToOutputFile()nicht für die Parameter überprüfen selbst, ist keine benutzerdefinierte Fehlermeldung angezeigt. Der Benutzer muss die resultierende Ausnahme selbst herausfinden.

Es läuft alles auf 3 hinaus. Schreiben Sie Code, der diese Funktionen tatsächlich verwendet, und vermeiden Sie Argumente, und erzeugen Sie die Fehlermeldung. Stellen Sie sich vor, Sie haben sich diese Funktionen überhaupt nicht angesehen und vertrauen nur auf ihre Namen, um ein ausreichendes Verständnis für die Verwendung zu gewährleisten. Wenn das alles ist, was Sie wissen, gibt es eine Möglichkeit, durch die Ausnahme verwirrt zu werden? Benötigen Sie eine benutzerdefinierte Fehlermeldung?

Es ist schwierig, den Teil Ihres Gehirns auszuschalten, der sich an die Innenseiten dieser Funktionen erinnert. So sehr, dass einige empfehlen, den using-Code vor dem verwendeten Code zu schreiben. Auf diese Weise kommen Sie zu dem Problem, dass Sie bereits wissen, wie die Dinge von außen aussehen. Sie müssen dazu kein TDD machen, aber wenn Sie TDD machen, kommen Sie bereits von außen herein.

kandierte_orange
quelle
4

Inwieweit Sie Ihre Methoden eigenständig und wiederverwendbar machen, ist eine gute Sache. Das bedeutet, dass Methoden vergeben sollten, was sie akzeptieren, und dass sie genau definierte Ergebnisse haben sollten (genau, was sie zurückgeben). Das bedeutet auch, dass sie in der Lage sein sollten, mit allem, was an sie weitergegeben wird , angemessen umzugehen und keine Annahmen über die Art des Inputs, die Qualität, das Timing usw. zu treffen.

Wenn ein Programmierer die Gewohnheit hat, Methoden zu schreiben, die Annahmen darüber treffen, was passiert, basierend auf Ideen wie "Wenn dies nicht funktioniert, müssen wir uns um größere Dinge kümmern" oder "Parameter X kann keinen Wert Y haben, weil der Rest von der Code verhindert es ", dann haben Sie plötzlich keine wirklich unabhängigen, entkoppelten Komponenten mehr. Ihre Komponenten sind im Wesentlichen vom weiteren System abhängig. Dies ist eine Art subtile enge Kopplung, die mit zunehmender Systemkomplexität zu einer exponentiellen Erhöhung der Gesamtbetriebskosten führt.

Beachten Sie, dass dies bedeuten kann, dass Sie dieselbe Information mehr als einmal validieren. Aber das ist in Ordnung. Jede Komponente ist auf ihre eigene Weise für ihre eigene Validierung verantwortlich . Dies ist keine Verletzung von DRY, da die Validierungen durch entkoppelte unabhängige Komponenten erfolgen und eine Änderung der Validierung in einer nicht unbedingt exakt in der anderen repliziert werden muss. Hier gibt es keine Redundanz. X hat die Verantwortung, seine Eingaben auf seine eigenen Bedürfnisse zu überprüfen und einige an Y weiterzuleiten. Y hat die Verantwortung, seine eigenen Eingaben auf seine Bedürfnisse zu überprüfen .

Brad Thomas
quelle
1

Angenommen, Sie haben eine Funktion (in C)

void readInputFile (const char* path);

Und Sie können keine Dokumentation über den Pfad finden. Und dann schauen Sie sich die Implementierung an und es heißt

void readInputFile (const char* path)
{
    assert (path != NULL && strlen (path) > 0);

Dadurch wird nicht nur die Eingabe für die Funktion getestet, sondern auch dem Benutzer der Funktion mitgeteilt, dass der Pfad nicht NULL oder eine leere Zeichenfolge sein darf.

gnasher729
quelle
0

Im Allgemeinen ist eine doppelte Überprüfung nicht immer gut oder schlecht. Es gibt immer viele Aspekte der Frage in Ihrem speziellen Fall, von denen die Angelegenheit abhängt. In deinem Fall:

  • Wie groß ist das Programm? Je kleiner es ist, desto offensichtlicher ist es, dass der Anrufer das Richtige tut. Wenn Ihr Programm größer wird, wird es wichtiger, genau anzugeben, welche Vor- und Nachbedingungen für jede Routine gelten.
  • Die Argumente werden bereits vom argparseModul geprüft . Es ist oft eine schlechte Idee, eine Bibliothek zu benutzen und dann ihre Arbeit selbst zu erledigen. Warum dann die Bibliothek nutzen?
  • Wie wahrscheinlich ist es, dass Ihre Methode in einem Kontext wiederverwendet wird, in dem der Aufrufer keine Argumente überprüft? Je wahrscheinlicher es ist, desto wichtiger ist es, Argumente zu validieren.
  • Was passiert , wenn ein Argument nicht verloren gehen? Wenn Sie keine Eingabedatei finden, wird die Verarbeitung wahrscheinlich sofort beendet. Dies ist wahrscheinlich ein offensichtlicher Fehlermodus, der leicht zu beheben ist. Die heimtückische Art von Fehlern ist die, bei der das Programm fröhlich weiterarbeitet und falsche Ergebnisse liefert, ohne dass Sie es bemerken .
Kilian Foth
quelle
0

Ihre Doppelkontrollen scheinen an Orten zu sein, an denen sie selten verwendet werden. Diese Überprüfungen machen Ihr Programm einfach robuster:

Ein zu großer Scheck tut nicht weh, einer zu wenig.

Wenn Sie jedoch in einer Schleife prüfen, die häufig wiederholt wird, sollten Sie überlegen, die Redundanz zu beseitigen, auch wenn die Prüfung selbst im Vergleich zu den nachfolgenden Schritten in den meisten Fällen nicht kostspielig ist.

qwerty_so
quelle
Und da Sie es bereits installiert haben, lohnt es sich nicht, es zu entfernen, es sei denn, es befindet sich in einer Schleife oder so.
StarWeaver
0

Vielleicht könnten Sie Ihre Sichtweise ändern:

Wenn etwas schief geht, was ist das Ergebnis? Schädigt es Ihre Anwendung / den Benutzer?

Man kann sich natürlich immer streiten, ob mehr oder weniger Schecks besser oder schlechter sind, aber das ist eine ziemlich schulische Frage. Und da Sie es zu tun realen Welt Software gibt es reale Welt Folgen.

Aus dem Kontext, den Sie angeben:

  • eine Eingabedatei A
  • eine Ausgabedatei B

Ich gehe davon aus, dass Sie eine Transformation von A nach B durchführen . Wenn A und B klein sind und die Transformation klein ist, was sind die Konsequenzen?

1) Sie haben vergessen anzugeben, woher gelesen werden soll: Dann ist das Ergebnis nichts . Und die Ausführungszeit wird kürzer sein als erwartet. Sie sehen sich das Ergebnis an - oder besser: Suchen Sie nach einem fehlenden Ergebnis, stellen Sie fest, dass Sie den Befehl falsch aufgerufen haben, beginnen Sie von vorne und alles ist wieder in Ordnung

2) Sie haben vergessen, die Ausgabedatei anzugeben. Daraus ergeben sich verschiedene Szenarien:

a) Die Eingabe wird sofort gelesen. Dann startet die Transformation und das Ergebnis sollte geschrieben werden, stattdessen erhalten Sie eine Fehlermeldung. Abhängig von der Zeit muss Ihr Benutzer warten (abhängig von der zu verarbeitenden Datenmenge), dies kann ärgerlich sein.

b) Die Eingabe wird schrittweise gelesen. Dann wird der Schreibvorgang wie in (1) sofort abgebrochen und der Benutzer beginnt von vorne.

Eine fehlerhafte Überprüfung kann unter bestimmten Umständen als OK angesehen werden. Es hängt ganz von Ihrem Verwendungszweck und Ihrer Absicht ab.

Zusätzlich: Sie sollten Paranoia vermeiden und nicht zu viele Doppelkontrollen durchführen.

Thomas Junk
quelle
0

Ich würde argumentieren, dass die Tests nicht überflüssig sind.

  • Sie haben zwei öffentliche Funktionen, für die ein Dateiname als Eingabeparameter erforderlich ist. Es ist angebracht, ihre Parameter zu validieren. Die Funktionen können möglicherweise in jedem Programm verwendet werden, das ihre Funktionalität benötigt.
  • Sie haben ein Programm, das zwei Argumente benötigt, die Dateinamen sein müssen. Zufällig werden die Funktionen verwendet. Es ist angebracht, dass das Programm seine Parameter überprüft.

Während die Dateinamen zweimal überprüft werden, werden sie für verschiedene Zwecke überprüft. In einem kleinen Programm, in dem Sie den Parametern vertrauen können, wurden die Prüfungen in den Funktionen als redundant angesehen.

Eine robustere Lösung hätte einen oder zwei Dateinamenprüfer.

  • Für eine Eingabedatei möchten Sie möglicherweise überprüfen, ob der Parameter eine lesbare Datei angegeben hat.
  • Für eine Ausgabedatei möchten Sie möglicherweise überprüfen, ob es sich bei dem Parameter um eine beschreibbare Datei oder einen gültigen Dateinamen handelt, der erstellt und beschrieben werden kann.

Ich verwende zwei Regeln für die Ausführung von Aktionen:

  • Mach sie so früh wie möglich. Dies funktioniert gut für Dinge, die immer benötigt werden. Aus Sicht dieses Programms ist dies die Überprüfung der argv-Werte, und nachfolgende Überprüfungen in der Programmlogik wären überflüssig. Wenn die Funktionen in eine Bibliothek verschoben werden, sind sie nicht mehr redundant, da die Bibliothek nicht darauf vertrauen kann, dass alle Aufrufer die Parameter validiert haben.
  • Mach sie so spät wie möglich. Dies funktioniert sehr gut für Dinge, die selten benötigt werden. Aus Sicht dieses Programms ist dies die Überprüfung der Funktionsparameter.
BillThor
quelle
0

Die Prüfung ist überflüssig. Um dies zu beheben, müssen Sie readFromInputFile und writeToOutputFile entfernen und durch readFromStream und writeToStream ersetzen.

An dem Punkt, an dem der Code den Dateistream empfängt, wissen Sie, dass ein gültiger Stream mit einer gültigen Datei verbunden ist oder mit was auch immer ein Stream verbunden werden kann. Dies vermeidet redundante Überprüfungen.

Dann könnten Sie fragen, ob Sie den Stream noch irgendwo öffnen müssen. Ja, aber das passiert intern in der Argument-Parsing-Methode. Sie haben dort zwei Überprüfungen, eine, um zu überprüfen, ob ein Dateiname erforderlich ist, die andere, um zu überprüfen, ob die Datei, auf die der Dateiname verweist, im angegebenen Kontext eine gültige Datei ist (z. B. Eingabedatei vorhanden, Ausgabeverzeichnis schreibbar). Dies sind verschiedene Arten von Überprüfungen, daher sind sie nicht redundant und finden innerhalb der Argument-Parsing-Methode (Anwendungsumfang) statt innerhalb der Kernanwendung statt.

Lüge Ryan
quelle