Programmierstilfrage zum Programmieren von Funktionen

8

Also, ich habe heute nur ein bisschen geschrieben, und mir ist aufgefallen, dass ich bei der Programmierung von Funktionen nicht viel Konsistenz habe, wenn es um einen Programmierstil geht. Eines meiner Hauptanliegen ist, ob es richtig kodiert ist oder nicht, so dass Sie überprüfen, ob die Eingabe des Benutzers außerhalb der Funktion gültig ist, oder einfach die vom Benutzer übergebenen Werte in die Funktion werfen und prüfen, ob die Werte gültig sind da drin. Lassen Sie mich ein Beispiel skizzieren:

Ich habe eine Funktion, die auf einer Umgebung basierende Hosts auflistet, und ich möchte die Umgebung in Teile von Hosts aufteilen können. Ein Beispiel für die Verwendung ist dies:

%Vor%

Dies wird alle Hosts von der "testenv", teilen Sie es in zwei Teile, und es zeigt Teil eins.

In meinem Code habe ich eine Funktion, die Sie in einer Liste übergeben, und sie gibt eine Liste von Listen basierend auf Ihren Parametern zum Teilen zurück. ABER, bevor ich eine Liste übergebe, überprüfe ich zuerst die Parameter in meinem MAIN während des getops-Prozesses, also überprüfe ich im Wesentlichen, um sicherzustellen, dass es keine vom Benutzer übergebenen Negative gibt. Ich stelle sicher, dass der Benutzer keine Aufteilung angefordert hat sagen wir, 4 Teile, aber fragend, Teil 5 anzuzeigen (was nicht gültig wäre), etc.

tl; dr: Würden Sie die Gültigkeit einer Benutzereingabe überprüfen, die den Fluss Ihrer MAIN-Klasse eingibt, oder würden Sie Ihre Funktion selbst überprüfen und im Falle einer gültigen Eingabe entweder eine gültige Antwort zurückgeben oder Bei ungültiger Eingabe NULL zurückgeben?

Offensichtlich funktionieren beide Methoden, ich bin nur daran interessiert, von Experten zu hören, welcher Ansatz besser ist :) Danke für Kommentare und Vorschläge, die ihr habt! Zu Ihrer Information, mein Beispiel ist in Python kodiert, aber ich bin immer noch mehr an einer allgemeinen Programmierantwort als an einer sprachspezifischen interessiert!

    
shawnjan 18.04.2010, 22:41
quelle

6 Antworten

5

Gute Frage! Mein wichtigster Rat ist, dass Sie das Problem systematisch angehen. Wenn Sie eine Funktion f entwerfen, hier ist, was ich über ihre Spezifikation denke:

  1. Welche absoluten Anforderungen muss ein Aufrufer von f erfüllen? Diese Anforderungen sind f 's Vorbedingung .

  2. Was macht f für seinen Aufrufer? Wenn f zurückgibt, was ist der Rückgabewert und wie ist der Zustand der Maschine? Unter welchen Umständen löst f eine Ausnahme aus und welche Ausnahme wird ausgelöst? Die Antworten auf all diese Fragen sind f 's Nachbedingung .

    Die Vorbedingung und die Nachbedingung zusammen bilden f s Vertrag mit Anrufern. Nur ein Anrufer, der die Voraussetzung erfüllt, muss sich auf die Nachbedingung verlassen.

  3. Was passiert schließlich, wenn der Aufrufer von f die Voraussetzung nicht erfüllt? Sie haben zwei Möglichkeiten:

    • Sie garantieren, das Programm zu stoppen, hofft man mit einer informativen Nachricht. Dies ist ein überprüfter Laufzeitfehler.

    • Alles ist möglich. Vielleicht gibt es einen segfault, vielleicht ist der Speicher beschädigt, vielleicht liefert f eine falsche Antwort. Dies ist ein unchecked Laufzeitfehler.

    Beachten Sie einige Elemente, die nicht in dieser Liste enthalten sind: Auslösen einer Ausnahme oder Zurückgeben eines Fehlercodes. Wenn auf diese Verhaltensweisen vertraut wird, werden sie Teil des Vertrags von f .

Jetzt kann ich deine Frage umformulieren:

  

Was sollte eine Funktion tun, wenn ihr Aufrufer gegen ihren Vertrag verstößt?

In den meisten Arten von Anwendungen sollte die Funktion das Programm mit einem checked Laufzeitfehler anhalten. Wenn das Programm Teil einer Anwendung ist, die zuverlässig sein muss, sollte die Anwendung entweder einen externen Mechanismus zum Neustarten einer Anwendung bereitstellen, die mit einem geprüften Laufzeitfehler anhält (im Erlang-Code üblich), oder wenn ein Neustart schwierig ist, alle Funktionen "Verträge sollten sehr freizügig sein, so dass" schlechter Input "immer noch den Vertrag erfüllt, aber immer eine Ausnahme verspricht.

In jedem Programm sollten unchecked Laufzeitfehler selten sein . Ein ungeprüfter Laufzeitfehler wird normalerweise nur aus Leistungsgründen gerechtfertigt, und selbst dann nur, wenn der Code performancekritisch ist. Eine weitere Quelle ungeprüfter Laufzeitfehler ist die Programmierung in unsicheren Sprachen. in C zum Beispiel gibt es keine Möglichkeit, zu überprüfen, ob der angegebene Speicher tatsächlich initialisiert wurde.

Ein weiterer Aspekt Ihrer Frage ist

  

Welche Arten von Verträgen machen die besten Designs?

Die Antwort auf diese Frage hängt mehr von der Problemdomäne ab. Da keine der Arbeiten, die ich mache, hoch verfügbar oder sicherheitskritisch sein muss, verwende ich restriktive Verträge und viele geprüfte Laufzeitfehler (typischerweise Assertionsfehler). Wenn Sie die Schnittstellen und Verträge eines großen Systems entwerfen, ist es viel einfacher, wenn Sie die Verträge einfach halten, die Vorbedingungen restriktiv (eng) halten und Sie auf geprüfte Laufzeitfehler vertrauen Argumente sind "schlecht".

  

Ich habe eine Funktion, die Sie in einer Liste übergeben, und sie gibt eine Liste von Listen basierend auf Ihren Parametern zum Teilen zurück. ABER, bevor ich eine Liste übergebe, überprüfe ich zuerst die Parameter in meinem MAIN während des getops-Prozesses, also überprüfe ich im Wesentlichen, um sicherzustellen, dass es keine vom Benutzer übergebenen Negative gibt. Ich stelle sicher, dass der Benutzer keine Aufteilung angefordert hat sagen wir, 4 Teile, aber fragend, Teil 5 anzuzeigen.

Ich denke, das ist genau der richtige Weg, um dieses spezielle Problem zu lösen:

  1. Ihr Vertrag mit dem Benutzer besteht darin, dass der Benutzer etwas sagen kann, und wenn der Benutzer eine unsinnige Anfrage macht, wird Ihr Programm nicht umfallen - es wird eine sinnvolle Fehlermeldung ausgeben und dann fortfahren.

    >
  2. Ihr interner Vertrag mit Ihrer Anfrage-Verarbeitungsfunktion ist, dass Sie nur vernünftige Anfragen weitergeben werden.

  3. Sie haben also außerhalb der Sekunde eine dritte Funktion, deren Aufgabe es ist, Sinn von Unsinn zu unterscheiden und entsprechend zu handeln - Ihre Anfrageverarbeitungsfunktion erhält "Sinn", der Benutzer wird von "Unsinn" und alle Verträge sind erfüllt.

  

Eines meiner Hauptanliegen ist, ob es richtig kodiert ist oder nicht, so dass Sie überprüfen, ob die Eingabe des Benutzers außerhalb der Funktion gültig ist.

Ja. Fast immer ist dies das beste Design. Tatsächlich gibt es wahrscheinlich irgendwo ein Designmuster mit einem ausgefallenen Namen. Aber wenn nicht, haben erfahrene Programmierer das immer wieder gesehen. Eines von zwei Dingen passiert:

  • parsen / validieren / ablehnen mit Fehlermeldung

  • parse / validiere / verarbeite

Diese Art von Design hat einen Datentyp (Anfrage) und vier Funktionen.Da ich diese Woche Tonnen von Haskell Code schreibe, werde ich ein Beispiel in Haskell geben:

%Vor%

Natürlich gibt es viele andere Möglichkeiten, dies zu tun. Fehler konnten sowohl in der Parsing-Phase als auch in der Validierungsphase entdeckt werden. "Gültige Anfrage" könnte tatsächlich durch einen anderen Typ als "nicht validierte Anfrage" dargestellt werden. Und so weiter.

    
Norman Ramsey 19.04.2010, 00:00
quelle
5

Ich würde den Check innerhalb der Funktion selbst durchführen, um sicherzustellen, dass die Parameter, die ich erwartet habe, tatsächlich das sind, was ich bekommen habe.

Nennen Sie es "defensive Programmierung" oder "Programmierung nach Vertrag" oder "Prüfparameter bestätigen" oder "Kapselung", aber die Idee ist, dass die Funktion dafür verantwortlich sein sollte, ihre eigenen Vor- und Nachbedingungen zu prüfen und sicherzustellen, dass Keine Invarianten werden verletzt.

Wenn Sie dies außerhalb der Funktion tun, lassen Sie sich die Möglichkeit offen, dass ein Kunde die Prüfungen nicht durchführt. Eine Methode sollte nicht darauf angewiesen sein, dass andere wissen, wie man sie richtig benutzt.

Wenn der Vertrag fehlschlägt, wirfst du entweder eine Ausnahme aus, falls deine Sprache dies unterstützt, oder gibst einen Fehlercode zurück.

    
duffymo 18.04.2010 22:46
quelle
2

Durch die Überprüfung innerhalb der Funktion wird die Komplexität erhöht. Daher besteht meine persönliche Richtlinie darin, die Plausibilitätsprüfung so weit wie möglich im Stack durchzuführen und Ausnahmen zu erfassen, sobald sie auftreten. Ich stelle auch sicher, dass meine Funktionen dokumentiert sind, damit andere Programmierer wissen, was die Funktion von ihnen erwartet. Sie folgen vielleicht nicht immer diesen Erwartungen, aber um ehrlich zu sein, ist es nicht meine Aufgabe, ihre Programme zum Laufen zu bringen.

    
Ignacio Vazquez-Abrams 18.04.2010 22:45
quelle
2

Es ist oft sinnvoll, die Eingabe an beiden Stellen zu überprüfen.

In der Funktion sollten Sie die Eingaben validieren und eine Ausnahme auslösen, wenn sie falsch sind. Dies verhindert ungültige Eingaben, die bewirken, dass die Funktion die Hälfte erreicht und dann eine unerwartete Ausnahme wie "Array-Index außerhalb der Grenzen" oder ähnliches auslöst. Dies wird Debugging-Fehler viel einfacher machen.

Das Auslösen von Ausnahmen sollte jedoch nicht als Flusssteuerung verwendet werden, und Sie möchten die rohe Ausnahme nicht direkt an den Benutzer übergeben. Daher würde ich auch Logik in die Benutzeroberfläche einfügen, um sicherzustellen, dass ich die Funktion nie mit ungültiger aufrufen werde Eingaben. In Ihrem Fall würde dies eine Meldung auf der Konsole anzeigen, aber in anderen Fällen zeigt es möglicherweise einen Validierungsfehler in einer GUI, möglicherweise während Sie tippen.

    
Mark Byers 18.04.2010 22:49
quelle
2

"Code Complete" schlägt eine Isolierungsstrategie vor, bei der man eine Linie zwischen Klassen zeichnen kann, die alle Eingaben validieren, und Klassen, die ihre Eingabe als bereits validiert behandeln. Alles, was die Validierungslinie passieren darf, gilt als sicher und kann an Funktionen übergeben werden, die keine Validierung durchführen (sie verwenden stattdessen Behauptungen, damit sich Fehler im externen Validierungscode manifestieren können).

    
GSerg 18.04.2010 23:19
quelle
1

Der Umgang mit Fehlern hängt von der Programmiersprache ab; Beim Schreiben einer Befehlszeilenanwendung sollte die Befehlszeile jedoch sicherstellen, dass die Eingabe sinnvoll ist. Wenn die Eingabe nicht sinnvoll ist, besteht das geeignete Verhalten darin, eine "Usage" -Nachricht mit einer Erklärung der Anforderungen auszudrucken und mit einem Statuscode ungleich Null zu beenden, damit andere Programme wissen, dass sie fehlgeschlagen ist (durch Testen des Exit-Codes). .

Silent failure ist der schlimmste Fehler , und genau das passiert, wenn Sie bei ungültigen Argumenten einfach falsche Ergebnisse zurückgeben. Wenn der Fehler jemals abgefangen wird, wird er höchstwahrscheinlich sehr weit entfernt vom wahren Fehlerpunkt entdeckt (das ungültige Argument übergeben). Daher ist es am besten, wenn IMHO eine Ausnahme auslöst (oder, falls nicht möglich, einen Fehlerstatuscode zurückgibt), wenn ein Argument ungültig ist, da es den Fehler sofort kennzeichnet, wenn es auftritt, wodurch es viel leichter zu identifizieren und zu korrigieren ist die wahre Ursache des Scheiterns.

Ich sollte auch hinzufügen, dass es sehr wichtig ist, konsistent zu sein, wie Sie mit ungültigen Eingaben umgehen; Sie sollten entweder eine Exception für eine ungültige Eingabe für alle Funktionen prüfen oder eine Ausnahme auslösen, da die Benutzer Ihrer Benutzeroberfläche feststellen, dass einige Funktionen ungültige Eingaben auslösen, sich auf dieses Verhalten verlassen und unglaublich überrascht sein werden wenn andere Funktionen einfach ungültige Ergebnisse zurückgeben anstatt sich zu beschweren.

    
Michael Aaron Safyan 18.04.2010 22:56
quelle