Guter Codestil, um überall Datenprüfungen einzuführen?

10

Ich habe ein Projekt, das so groß ist, dass ich nicht mehr jeden Aspekt im Kopf behalten kann. Ich beschäftige mich mit einer Reihe von Klassen und Funktionen darin und gebe Daten weiter.

Mit der Zeit bemerkte ich, dass ich immer wieder Fehler bekam, weil ich vergaß, welche genaue Form die Daten haben müssen, wenn ich sie an verschiedene Funktionen übergebe ( z. B. eine Funktion akzeptiert und gibt ein Array von Zeichenfolgen aus, eine andere Funktion, die ich viel später geschrieben habe). Akzeptiert Zeichenfolgen, die in einem Wörterbuch usw. gespeichert sind. Daher muss ich die Zeichenfolgen, mit denen ich arbeite, von einem Array in ein Wörterbuch umwandeln .

Um nicht immer herausfinden zu müssen, was wo kaputt gegangen ist, habe ich begonnen, jede Funktion und Klasse als "isolierte Entität" in dem Sinne zu behandeln, dass sie sich nicht darauf verlassen kann, dass externer Code die richtige Eingabe liefert, und dass sie selbst Eingabeprüfungen durchführen muss (oder) In einigen Fällen müssen die Daten neu gefasst werden, wenn die Daten in der falschen Form angegeben wurden.

Dies hat die Zeit, die ich damit verbringe, sicherzustellen, dass die Daten, die ich weitergebe, in jede Funktion "passen", erheblich reduziert, da mich die Klassen und Funktionen selbst jetzt warnen, wenn einige Eingaben schlecht sind (und dies manchmal sogar korrigieren), und ich nicht Ich muss mit einem Debugger den gesamten Code durchgehen, um herauszufinden, wo etwas durcheinander geraten ist.

Andererseits hat dies auch den Gesamtcode erhöht.
Meine Frage ist, ob dieser Codestil zur Lösung dieses Problems geeignet ist.
Die beste Lösung wäre natürlich, das Projekt komplett umzugestalten und sicherzustellen, dass die Daten für alle Funktionen eine einheitliche Struktur haben. Da dieses Projekt jedoch ständig wächst, würde ich am Ende mehr Geld ausgeben und mir Sorgen um sauberen Code machen, als tatsächlich neues Material hinzuzufügen .

(Zu Ihrer Information: Ich bin noch ein Anfänger. Bitte entschuldigen Sie, wenn diese Frage naiv war. Mein Projekt ist in Python.)

user7088941
quelle
1
Mögliches Duplikat von Wie defensiv sollten wir sein?
Mücke
3
@gnat Es ist ähnlich, aber anstatt meine Frage zu beantworten, gibt es dort Ratschläge ("Sei so defensiv wie du kannst") für die von OP erwähnte spezifische Instanz , die sich von meiner allgemeineren Abfrage unterscheidet.
user7088941
2
"Aber da dieses Projekt ständig wächst, würde ich am Ende mehr Geld ausgeben und mir Sorgen um sauberen Code machen, als tatsächlich neue Dinge hinzuzufügen" - das klingt so, als müssten Sie sich um sauberen Code sorgen. Andernfalls wird Ihre Produktivität immer langsamer, da das Hinzufügen neuer Funktionen aufgrund des vorhandenen Codes immer schwieriger wird. Nicht jedes Refactoring muss "vollständig" sein. Wenn das Hinzufügen von etwas Neuem aufgrund des vorhandenen Codes, den es berührt, schwierig ist, überarbeiten Sie nur diesen berührenden Code und notieren Sie sich, was Sie später noch einmal besuchen
möchten
3
Dies ist ein Problem, mit dem Menschen häufig konfrontiert sind, wenn sie schwach typisierte Sprachen verwenden. Wenn Sie nicht in eine streng typisierte Sprache wechseln möchten oder können, lautet die Antwort einfach "Ja, dieser Codestil ist zur Lösung dieses Problems geeignet" . Nächste Frage?
Doc Brown
1
In einer streng typisierten Sprache mit definierten Datentypen hätte der Compiler dies für Sie erledigt.
SD

Antworten:

4

Eine bessere Lösung besteht darin, die Funktionen und Werkzeuge der Python-Sprache besser zu nutzen.

In Funktion 1 ist die erwartete Eingabe beispielsweise ein Array von Zeichenfolgen, wobei die erste Zeichenfolge den Titel von etwas und die zweite eine bibliografische Referenz angibt. In Funktion 2 ist die erwartete Eingabe immer noch ein Array von Zeichenfolgen, aber jetzt sind die Rollen der Zeichenfolgen umgekehrt.

Dieses Problem wird durch a gemildert namedtuple. Es ist leichtgewichtig und gibt den Mitgliedern Ihres Arrays eine einfache semantische Bedeutung.

Um die Vorteile einer automatischen Typprüfung zu nutzen, ohne die Sprache zu wechseln, können Sie die Typhinweise nutzen . Eine gute IDE kann dies verwenden, um Sie zu informieren, wenn Sie etwas Dummes tun.

Sie scheinen auch besorgt darüber zu sein, dass Funktionen veraltet sind, wenn sich Anforderungen ändern. Dies kann durch automatisierte Tests erfasst werden .

Ich sage zwar nicht, dass eine manuelle Überprüfung niemals angemessen ist, aber eine bessere Verwendung der verfügbaren Sprachfunktionen kann Ihnen helfen, dieses Problem auf eine wartbarere Weise zu lösen.


quelle
+1 für das Zeigen auf mich namedtupleund all die anderen schönen Dinge. Ich habe nicht darüber nachgedacht namedtuple- und obwohl ich über automatisierte Tests Bescheid wusste, habe ich es nie wirklich oft benutzt und wusste nicht, wie sehr es mir in diesem Fall helfen würde. All dies scheint wirklich so gut zu sein wie eine statische Analyse. (Automatisierte Tests sind möglicherweise sogar besser, da ich alle subtilen Dinge erfassen kann, die bei einer statischen Analyse nicht erfasst würden!) Wenn Sie andere kennen, lassen Sie es mich bitte wissen. Ich werde die Frage noch eine Weile offen halten, aber wenn keine anderen Antworten kommen, werde ich Ihre akzeptieren.
user7088941
9

OK, das eigentliche Problem wird in einem Kommentar unter dieser Antwort beschrieben:

In Funktion 1 ist die erwartete Eingabe beispielsweise ein Array von Zeichenfolgen, wobei die erste Zeichenfolge den Titel von etwas und die zweite eine bibliografische Referenz angibt. In Funktion 2 ist die erwartete Eingabe immer noch ein Array von Zeichenfolgen, aber jetzt sind die Rollen der Zeichenfolgen umgekehrt

Das Problem hierbei ist die Verwendung einer Liste von Zeichenfolgen, bei denen die Reihenfolge die Semantik bedeutet. Dies ist ein wirklich fehleranfälliger Ansatz. Stattdessen sollten Sie eine benutzerdefinierte Klasse mit zwei Feldern mit dem Namen titleund erstellen bibliographical_reference. Auf diese Weise werden Sie sie nicht verwechseln und dieses Problem in Zukunft vermeiden. Dies erfordert natürlich einige Umgestaltungen, wenn Sie bereits an vielen Stellen Listen mit Zeichenfolgen verwenden, aber glauben Sie mir, es wird auf lange Sicht billiger sein.

Der gängige Ansatz in Sprachen mit dynamischer Typisierung ist "Ententypisierung". Dies bedeutet, dass Sie sich nicht wirklich für den "Typ" des übergebenen Objekts interessieren. Es ist Ihnen nur wichtig, ob er die von Ihnen aufgerufenen Methoden unterstützt. In Ihrem Fall lesen Sie einfach das aufgerufene Feld, bibliographical_referencewenn Sie es benötigen. Wenn dieses Feld für das übergebene Objekt nicht vorhanden ist, wird eine Fehlermeldung angezeigt. Dies zeigt an, dass der falsche Typ an die Funktion übergeben wurde. Dies ist eine ebenso gute Typprüfung wie jede andere.

JacquesB
quelle
Manchmal ist das Problem sogar noch subtiler: Ich übergebe den richtigen Typ, aber die "interne Struktur" meiner Eingabe bringt die Funktion durcheinander: In Funktion 1 ist die erwartete Eingabe beispielsweise ein Array von Zeichenfolgen, wobei die erste Zeichenfolge bezeichnet den Titel von etwas und der zweite einen Literaturhinweis. In Funktion 2 ist die erwartete Eingabe immer noch ein Array von Zeichenfolgen, aber jetzt sind die Rollen der Zeichenfolgen umgekehrt: Die erste Zeichenfolge sollte die bibliografische Referenz und die zweite die bibliografische Referenz sein. Ich denke für diese Schecks sind angebracht?
user7088941
1
@ user7088941: Das von Ihnen beschriebene Problem kann leicht gelöst werden, indem eine Klasse mit zwei Feldern vorhanden ist: "title" und "bibliographical_reference". Sie werden das nicht verwechseln. Sich auf die Reihenfolge in einer Liste von Zeichenfolgen zu verlassen, scheint sehr fehleranfällig zu sein. Vielleicht ist dies das zugrunde liegende Problem?
JacquesB
3
Das ist die Antwort. Python ist eine objektorientierte Sprache, keine Liste von Wörterbüchern mit Zeichenfolgen und ganzen Zahlen (oder was auch immer). Verwenden Sie also Objekte. Objekte sind dafür verantwortlich, ihren eigenen Status zu verwalten und ihre eigenen Invarianten zu erzwingen. Andere Objekte können sie niemals beschädigen (wenn sie richtig entworfen wurden). Wenn unstrukturierte oder halbstrukturierte Daten von außen in Ihr System gelangen, validieren und analysieren Sie sie einmal an der Systemgrenze und konvertieren sie so schnell wie möglich in umfangreiche Objekte.
Jörg W Mittag
3
"Ich würde ständiges Refactoring wirklich vermeiden" - diese mentale Blockade ist Ihr Problem. Guter Code entsteht nur durch Refactoring. Viel Refactoring. Unterstützt durch Unit-Tests. Besonders wenn Komponenten erweitert oder weiterentwickelt werden müssen.
Doc Brown
2
Ich hab es jetzt. +1 für all die schönen Einblicke und Kommentare. Und vielen Dank an alle für ihre unglaublich hilfreichen Kommentare! (Während ich einige Klassen / Objekte verwendete, habe ich sie mit den genannten Listen durchsetzt, was, wie ich jetzt sehe, keine gute Idee war. Die Frage blieb, wie dies am besten umgesetzt werden kann, wobei ich die konkreten Vorschläge aus der Antwort von JETM verwendete , was wirklich einen radikalen Unterschied in Bezug auf die Geschwindigkeit des Erreichens eines fehlerfreien Zustands machte.)
user7088941
3

Zuallererst erleben Sie gerade Code-Geruch - versuchen Sie sich zu erinnern, was dazu geführt hat, dass Sie sich des Geruchs bewusst werden, und versuchen Sie, Ihre "mentale" Nase zu schärfen. Je früher Sie einen Code-Geruch bemerken, desto schneller - und einfacher - Sie können das zugrunde liegende Problem beheben.

Um nicht immer herausfinden zu müssen, was wo kaputt gegangen ist, habe ich begonnen, jede Funktion und Klasse als "isolierte Entität" in dem Sinne zu behandeln, dass sie sich nicht auf externen Code verlassen kann, der ihr die richtige Eingabe gibt, und selbst Eingabeprüfungen durchführen muss.

Defensive Programmierung - wie diese Technik genannt wird - ist ein gültiges und häufig verwendetes Werkzeug. Wie bei allen Dingen ist es jedoch wichtig, die richtige Menge zu verwenden, zu wenig Schecks und Sie werden keine Probleme bemerken, zu viele und Ihr Code wird überfüllt sein.

(oder in einigen Fällen die Daten neu formulieren, wenn die Daten in der falschen Form angegeben sind).

Das könnte eine weniger gute Idee sein. Wenn Sie feststellen, dass ein Teil Ihres Programms eine Funktion mit falsch formatierten Daten aufruft, FIX THAT PART , ändern Sie die aufgerufene Funktion nicht, um trotzdem fehlerhafte Daten verarbeiten zu können.

Dies hat die Zeit, die ich damit verbringe, sicherzustellen, dass die Daten, die ich weitergebe, in jede Funktion "passen", erheblich reduziert, da mich die Klassen und Funktionen selbst jetzt warnen, wenn einige Eingaben schlecht sind (und dies manchmal sogar korrigieren), und ich nicht Ich muss mit einem Debugger den gesamten Code durchgehen, um herauszufinden, wo etwas durcheinander geraten ist.

Die Verbesserung der Qualität und Wartbarkeit Ihres Codes spart auf lange Sicht Zeit (in diesem Sinne muss ich erneut vor der Selbstkorrekturfunktion warnen, die Sie in einige Ihrer Funktionen integriert haben - sie können eine heimtückische Quelle für Fehler sein. Nur weil Ihre Programm stürzt nicht ab & brennen heißt nicht, dass es richtig funktioniert ...)

Um Ihre Frage endgültig zu beantworten: Ja, defensive Programmierung (dh Überprüfung der Gültigkeit der bereitgestellten Parameter) ist - in einem gesunden Maße - eine gute Strategie. , Daß das , wie Sie selbst gesagt, Ihr Code ist inconsitent, und ich würde stark reccommend , dass Sie einige Zeit damit verbringen , um die Teile Refactoring , die riechen - Sie sagen , Sie wollen nicht zu befürchten sauberen Code die ganze Zeit, verbringen mehr Zeit auf "Bereinigen" als bei neuen Funktionen ... Wenn Sie Ihren Code nicht sauber halten, verbringen Sie möglicherweise doppelt so viel Zeit mit "Speichern", wenn Sie keinen sauberen Code zum Beseitigen von Fehlern aufbewahren UND es schwierig sein wird, neue Funktionen zu implementieren - technische Schulden können zerquetschen Sie.

CharonX
quelle
1

Das ist okay. Ich habe in FoxPro programmiert, wo ich fast in jeder großen Funktion einen TRY..CATCH-Block hatte. Jetzt codiere ich in JavaScript / LiveScript und überprüfe selten Parameter in "internen" oder "privaten" Funktionen.

"Wie viel zu überprüfen ist" hängt mehr vom gewählten Projekt / der gewählten Sprache ab als von Ihren Code-Fähigkeiten.

Michael Quad
quelle
1
Ich denke, es war TRY ... CATCH ... IGNORE. Sie haben ungefähr das Gegenteil von dem getan, was das OP verlangt. Meiner Meinung nach geht es darum, Inkonsistenzen zu vermeiden, während Sie dafür gesorgt haben, dass das Programm nicht explodiert, wenn Sie eines treffen.
Maaartinus
1
@maaartinus das ist richtig. Mit Programmiersprachen erhalten wir normalerweise Konstrukte, die einfach zu verwenden sind, um die Anwendung von Sprengungen zu verhindern. Die Programmiersprachen für Konstrukte zur Vermeidung von Inkonsistenzen scheinen jedoch viel schwieriger zu verwenden: Meines Wissens überarbeiten Sie ständig alles und verwenden Sie Klassen, die am besten containerisieren Informationsfluss in Ihrer Anwendung. Genau darum frage ich - gibt es einen einfacheren Weg, dies zu beheben?
user7088941
@ user7088941 Deshalb vermeide ich schwach getippte Sprachen. Python ist einfach fantastisch, aber für etwas Größeres kann ich nicht verfolgen, was ich anderswo gemacht habe. Daher bevorzuge ich Java, das ziemlich ausführlich ist (nicht so sehr mit Lombok- und Java 8-Funktionen), strikte Typisierung und Werkzeuge für die statische Analyse hat. Ich würde vorschlagen, dass Sie einige streng typisierte Sprachen ausprobieren, da ich nicht weiß, wie ich sie sonst lösen soll.
Maaartinus
Es geht nicht um strenge / lose typisierte Parameter. Es geht darum zu wissen, dass der Parameter korrekt ist. Selbst wenn Sie (Ganzzahl 4 Byte) verwenden, müssen Sie möglicherweise überprüfen, ob es beispielsweise in einem Bereich von 0 bis 10 liegt. Wenn Sie wissen, dass der Parameter immer 0..10 ist, müssen Sie ihn nicht überprüfen. FoxPro hat zum Beispiel keine assoziativen Arrays, es ist sehr schwierig, mit seinen Variablen, ihrem Umfang usw. zu arbeiten. Deshalb müssen Sie check check überprüfen.
Michael Quad
1
@ user7088941 Es ist nicht OO, aber es gibt die Regel "schnell scheitern". Jede nicht-private Methode muss ihre Argumente überprüfen und werfen, wenn etwas nicht stimmt. Kein Versuch, es zu reparieren, kein Versuch, es zu reparieren, blase es einfach in den Himmel. Sicher, auf einer höheren Ebene wird die Ausnahme protokolliert und behandelt. Da Ihre Tests die meisten Probleme im Voraus feststellen und keine Probleme verborgen werden, konvergiert der Code viel schneller zu einer fehlerfreien Lösung als bei Fehlertoleranz.
Maaartinus