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 ...
- 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 - haben eine Funktion,
readFromInputFile
die zuerst prüft, ob ein Eingabedateiname eingegeben wurde - haben eine Funktion,
writeToOutputFile
die 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 if
Bedingung 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 except
in 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!
quelle
Antworten:
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
read
undwrite
-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
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 ÄnderungwriteToOutputFile
: 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.quelle
Redundanz ist nicht die Sünde. Überflüssige Redundanz ist.
Wenn
readFromInputFile()
undwriteToOutputFile()
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.Wenn
readFromInputFile()
undwriteToOutputFile()
sich für die Parameter überprüfen, erhalten Sie erneut eine benutzerdefinierte Fehlermeldung zu zeigen , dass die Notwendigkeit für Dateinamen erklärt.Wenn
readFromInputFile()
undwriteToOutputFile()
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.
quelle
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 .
quelle
Angenommen, Sie haben eine Funktion (in C)
Und Sie können keine Dokumentation über den Pfad finden. Und dann schauen Sie sich die Implementierung an und es heißt
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.
quelle
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:
argparse
Modul geprüft . Es ist oft eine schlechte Idee, eine Bibliothek zu benutzen und dann ihre Arbeit selbst zu erledigen. Warum dann die Bibliothek nutzen?quelle
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.
quelle
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:
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.
quelle
Ich würde argumentieren, dass die Tests nicht überflüssig sind.
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.
Ich verwende zwei Regeln für die Ausführung von Aktionen:
quelle
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.
quelle