Validierung des Eingabeparameters beim Aufrufer: Codeduplizierung?

16

Wo kann man Eingabeparameter einer Funktion am besten validieren: im Aufrufer oder in der Funktion selbst?

Da ich meinen Codierungsstil verbessern möchte, versuche ich, die Best Practices oder einige Regeln für dieses Problem zu finden. Wann und was ist besser.

In meinen vorherigen Projekten haben wir jeden Eingabeparameter in der Funktion überprüft und behandelt (z. B. wenn er nicht null ist). Nun habe ich hier in einigen Antworten und auch im Pragmatic Programmer-Buch gelesen, dass die Validierung der Eingabeparameter Aufgabe des Aufrufers ist.

Das bedeutet, dass ich die Eingabeparameter validieren sollte, bevor ich die Funktion aufrufe. Überall wird die Funktion aufgerufen. Und das wirft eine Frage auf: Erzeugt es nicht überall, wo die Funktion aufgerufen wird, eine Verdoppelung der Prüfbedingung?

Ich interessiere mich nicht nur für Nullbedingungen, sondern für die Validierung von Eingabevariablen (negativer Wert für die sqrtFunktion, Division durch Null, falsche Kombination von Bundesland und Postleitzahl oder irgendetwas anderes)

Gibt es einige Regeln, um zu entscheiden, wo die Eingabebedingung überprüft werden soll?

Ich denke über einige Argumente nach:

  • Wenn die Behandlung von ungültigen Variablen variieren kann, ist es gut, sie auf der Anruferseite zu validieren (z. B. sqrt()Funktion - in einigen Fällen möchte ich möglicherweise mit komplexen Zahlen arbeiten, also behandle ich die Bedingung im Anrufer).
  • Wenn die Überprüfungsbedingung bei jedem Aufrufer gleich ist, ist es besser, sie in der Funktion zu überprüfen, um Doppelungen zu vermeiden
  • Die Validierung der Eingabeparameter im Aufrufer erfolgt nur einmal vor dem Aufruf vieler Funktionen mit diesem Parameter. Daher ist die Validierung eines Parameters in jeder Funktion nicht wirksam
  • Die richtige Lösung hängt vom jeweiligen Fall ab

Ich hoffe, diese Frage ist kein Duplikat einer anderen, ich habe nach diesem Problem gesucht und ähnliche Fragen gefunden, aber sie erwähnen nicht genau diesen Fall.

Srnka
quelle

Antworten:

15

Es hängt davon ab, ob. Die Entscheidung darüber, wo die Validierung erfolgen soll, sollte auf der Beschreibung und der Stärke des Vertrags beruhen, der von der Methode impliziert (oder dokumentiert) wird. Die Validierung ist ein guter Weg, um die Einhaltung eines bestimmten Vertrags zu stärken. Wenn die Methode aus irgendeinem Grund einen sehr strengen Vertrag hat, müssen Sie dies vor dem Anruf überprüfen.

Dies ist ein besonders wichtiges Konzept, wenn Sie eine öffentliche Methode erstellen , da Sie im Grunde dafür werben, dass eine Methode eine Operation ausführt. Es ist besser, das zu tun, was du sagst!

Nehmen Sie die folgende Methode als Beispiel:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

Was beinhaltet der Vertrag DeletePerson? Der Programmierer kann nur davon ausgehen, dass eventuell Personübergebene Daten gelöscht werden. Wir wissen jedoch, dass dies nicht immer der Fall ist. Was ist, wenn pein nullWert ist? Was ist, wenn pes in der Datenbank keine gibt? Was ist, wenn die Datenbank getrennt ist? Daher scheint DeletePerson seinen Vertrag nicht gut zu erfüllen. Manchmal löscht es eine Person, und manchmal löst es eine NullReferenceException oder eine DatabaseNotConnectedException aus, oder manchmal führt es nichts aus (z. B. wenn die Person bereits gelöscht wurde).

APIs wie diese sind notorisch schwierig zu verwenden, denn wenn Sie diese "Black Box" einer Methode nennen, können alle möglichen schrecklichen Dinge passieren.

Hier sind ein paar Möglichkeiten, wie Sie den Vertrag verbessern können:

  • Fügen Sie eine Validierung hinzu und fügen Sie dem Vertrag eine Ausnahme hinzu. Dies macht den Vertrag stärker , aber erfordert , dass die Anrufer - Validierung durchzuführen. Der Unterschied ist jedoch, dass sie jetzt ihre Anforderungen kennen. In diesem Fall kommuniziere ich dies mit einem C # XML-Kommentar, aber Sie können stattdessen ein throws(Java), ein Assertoder ein Vertragstool wie Code Contracts hinzufügen .

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

    Randnotiz: Das Argument gegen diesen Stil ist oft, dass er eine übermäßige Vorprüfung durch den gesamten aufrufenden Code verursacht, aber meiner Erfahrung nach ist dies oft nicht der Fall. Stellen Sie sich ein Szenario vor, in dem Sie versuchen, eine leere Person zu löschen. Wie ist das passiert? Woher kam die null Person? Wenn dies beispielsweise eine Benutzeroberfläche ist, warum wurde die Entf-Taste behandelt, wenn keine aktuelle Auswahl vorhanden ist? Wenn es bereits gelöscht wurde, sollte es nicht bereits von der Anzeige entfernt worden sein? Offensichtlich gibt es Ausnahmen, aber wenn ein Projekt wächst, werden Sie häufig solchen Code dafür danken, dass er verhindert, dass Fehler tief in das System eindringen.

  • Validierung und Code defensiv hinzufügen. Dies macht den Vertrag lockerer , da diese Methode jetzt mehr als nur das Löschen der Person bewirkt. Ich habe den Methodennamen geändert, um dies widerzuspiegeln, er ist jedoch möglicherweise nicht erforderlich, wenn Sie in Ihrer API konsistent sind. Dieser Ansatz hat Vor- und Nachteile. Der Vorteil ist, dass Sie jetzt TryDeletePersonalle Arten von ungültigen Eingaben übergeben können und sich keine Gedanken über Ausnahmen machen müssen. Das Gegenteil ist natürlich, dass Benutzer Ihres Codes diese Methode wahrscheinlich zu oft aufrufen oder das Debuggen in Fällen, in denen p null ist, erschweren. Dies könnte als leichte Verletzung des Grundsatzes der einheitlichen Verantwortung angesehen werden. Denken Sie also daran, wenn ein Flammenkrieg ausbricht.

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • Ansätze kombinieren. Manchmal möchten Sie ein wenig von beidem, wenn Sie möchten, dass externe Anrufer die Regeln genau befolgen (um sie zum verantwortlichen Code zu zwingen), aber Sie möchten, dass Ihr privater Code flexibel ist.

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

Nach meiner Erfahrung funktioniert es am besten, sich auf die Verträge zu konzentrieren, die Sie angedeutet haben, und nicht auf eine harte Regel. Defensive Codierung scheint in Fällen, in denen der Aufrufer nur schwer oder gar nicht feststellen kann, ob eine Operation gültig ist, besser zu funktionieren. Strikte Verträge scheinen besser zu funktionieren, wenn Sie erwarten, dass der Aufrufer Methodenaufrufe nur dann ausführt, wenn sie wirklich, wirklich sinnvoll sind.

Kevin McCormick
quelle
Danke für die sehr nette Antwort mit Beispiel. Ich mag den Punkt "defensiv" und "streng vertraglich".
Srnka
7

Es ist eine Frage der Konvention, Dokumentation und des Anwendungsfalls.

Nicht alle Funktionen sind gleich. Nicht alle Anforderungen sind gleich. Nicht alle Validierungen sind gleich.

Wenn Ihr Java-Projekt beispielsweise nach Möglichkeit versucht, Nullzeiger zu vermeiden (siehe z. B. die Empfehlungen im Guava-Stil ), überprüfen Sie trotzdem jedes Funktionsargument, um sicherzustellen, dass es nicht Null ist? Es ist wahrscheinlich nicht notwendig, aber es besteht die Möglichkeit, dass Sie es trotzdem tun, um das Auffinden von Fehlern zu erleichtern. Sie können jedoch eine Zusicherung verwenden, bei der Sie zuvor eine NullPointerException ausgelöst haben.

Was ist, wenn das Projekt in C ++ ist? Konvention / Tradition in C ++ ist es, Vorbedingungen zu dokumentieren, sie jedoch (wenn überhaupt) nur in Debugbuilds zu überprüfen.

In beiden Fällen haben Sie eine dokumentierte Voraussetzung für Ihre Funktion: Kein Argument darf null sein. Sie können stattdessen die Domäne der Funktion erweitern, um Nullen mit definiertem Verhalten einzuschließen, z. B. "Wenn ein Argument Null ist, wird eine Ausnahme ausgelöst". Das ist natürlich wieder mein C ++ - Erbe - in Java ist es üblich genug, Vorbedingungen auf diese Weise zu dokumentieren.

Es können aber nicht alle Voraussetzungen auch nur zumutbar überprüft werden. Ein binärer Suchalgorithmus setzt beispielsweise voraus, dass die zu durchsuchende Sequenz sortiert werden muss. Die Überprüfung, ob dies tatsächlich der Fall ist, ist jedoch eine O (N) -Operation. Wenn Sie dies also bei jedem Aufruf tun, verlieren Sie den Punkt, an dem Sie überhaupt einen O (log (N)) -Algorithmus verwenden. Wenn Sie defensiv programmieren, können Sie kleinere Überprüfungen durchführen (z. B. um sicherzustellen, dass für jede Partition, die Sie durchsuchen, die Start-, Mittel- und Endwerte sortiert sind), aber dies erfasst nicht alle Fehler. In der Regel müssen Sie sich nur darauf verlassen, dass die Voraussetzung erfüllt ist.

Der einzige reale Ort, an dem Sie explizite Prüfungen benötigen, liegt an den Grenzen. Externer Input für Ihr Projekt? Validieren, validieren, validieren. Ein grauer Bereich ist API-Grenzen. Es hängt wirklich davon ab, wie sehr Sie dem Client-Code vertrauen möchten, wie viel Schaden ungültige Eingaben anrichten und wie viel Unterstützung Sie beim Auffinden von Fehlern leisten möchten. Jede Berechtigungsgrenze muss natürlich als extern gelten - Syscalls werden beispielsweise in einem Kontext mit erhöhten Berechtigungen ausgeführt und müssen daher sehr sorgfältig validiert werden. Eine solche Validierung muss natürlich systemintern erfolgen.

Sebastian Redl
quelle
Danke für deine Antwort. Können Sie bitte den Link zur Guavenstilempfehlung geben? Ich kann nicht googeln und herausfinden, was Sie damit gemeint haben. +1 für die Validierung der Grenzen.
Srnka
Link hinzugefügt. Es ist eigentlich kein vollständiger Styleguide, sondern nur ein Teil der Dokumentation der Nicht-Null-Dienstprogramme.
Sebastian Redl
6

Die Parameterüberprüfung sollte das Anliegen der aufgerufenen Funktion sein. Die Funktion sollte wissen, was als gültige Eingabe gilt und was nicht. Anrufer wissen dies möglicherweise nicht, insbesondere wenn sie nicht wissen, wie die Funktion intern implementiert ist. Es ist zu erwarten, dass die Funktion jede Kombination von Parameterwerten von Aufrufern verarbeitet.

Da die Funktion für die Überprüfung von Parametern verantwortlich ist, können Sie Komponententests für diese Funktion schreiben, um sicherzustellen, dass sie sich sowohl für gültige als auch für ungültige Parameterwerte wie beabsichtigt verhält.

Bernard
quelle
Danke für die Antwort. Sie denken also, diese Funktion sollte in jedem Fall sowohl gültige als auch ungültige Eingabeparameter prüfen. Etwas anderes als die Bestätigung des Pragmatic Programmer-Buches: "Die Validierung der Eingabeparameter liegt in der Verantwortung des Aufrufers". Es ist nett gedacht "Die Funktion sollte wissen, was als gültig angesehen wird ... Anrufer können dies nicht wissen" ... Sie möchten also keine Vorbedingungen verwenden?
Srnka
1
Sie können Vorbedingungen verwenden, wenn Sie möchten (siehe Sebastians Antwort ), aber ich bevorzuge es, defensiv zu sein und mit jeder Art von möglicher Eingabe umzugehen.
Bernard
4

Innerhalb der Funktion selbst. Wenn die Funktion mehrmals verwendet wird, möchten Sie den Parameter nicht für jeden Funktionsaufruf überprüfen.

Wenn die Funktion so aktualisiert wird, dass die Validierung des Parameters beeinträchtigt wird, müssen Sie nach jedem Auftreten der Aufrufervalidierung suchen, um sie zu aktualisieren. Es ist nicht schön :-).

Sie können sich auf die Schutzklausel beziehen

Aktualisieren

Siehe meine Antwort für jedes von Ihnen bereitgestellte Szenario.

  • Wenn die Behandlung von ungültigen Variablen variieren kann, ist es gut, sie auf der Anruferseite zu validieren (z. B. sqrt()Funktion - in einigen Fällen möchte ich möglicherweise mit komplexen Zahlen arbeiten, also behandle ich die Bedingung im Anrufer).

    Antworten

    Die Mehrheit der Programmiersprachen unterstützt standardmäßig ganze und reelle Zahlen, keine komplexen Zahlen, daher sqrtakzeptiert ihre Implementierung nur nicht negative Zahlen. Der einzige Fall, in dem Sie eine sqrtFunktion haben, die eine komplexe Zahl zurückgibt, ist die Verwendung einer auf Mathematik ausgerichteten Programmiersprache wie Mathematica

    Darüber hinaus ist sqrtfür die meisten Programmiersprachen bereits eine Implementierung implementiert, sodass Sie diese nicht ändern können. Wenn Sie versuchen, die Implementierung zu ersetzen (siehe Patching für Affen), sind Ihre Mitarbeiter zutiefst geschockt, warum sqrtplötzlich negative Zahlen akzeptiert werden.

    Wenn Sie eine möchten, können Sie sie um Ihre benutzerdefinierte sqrtFunktion wickeln , die negative Zahlen verarbeitet und komplexe Zahlen zurückgibt.

  • Wenn die Überprüfungsbedingung bei jedem Aufrufer gleich ist, ist es besser, sie in der Funktion zu überprüfen, um Doppelungen zu vermeiden

    Antworten

    Ja, dies ist eine gute Vorgehensweise, um eine Streuung der Parameterüberprüfung in Ihrem Code zu vermeiden.

  • Die Validierung der Eingabeparameter im Aufrufer erfolgt nur einmal vor dem Aufruf vieler Funktionen mit diesem Parameter. Daher ist die Validierung eines Parameters in jeder Funktion nicht wirksam

    Antworten

    Es wird schön sein, wenn der Anrufer eine Funktion ist, meinst du nicht auch?

    Wenn die Funktionen innerhalb des Aufrufers von einem anderen Aufrufer verwendet werden, was hindert Sie daran, den Parameter innerhalb der vom Aufrufer aufgerufenen Funktionen zu validieren?

  • Die richtige Lösung hängt vom jeweiligen Fall ab

    Antworten

    Streben Sie nach wartbarem Code. Durch das Verschieben der Parametervalidierung wird sichergestellt, dass eine Quelle der Wahrheit darüber vorliegt, was die Funktion akzeptieren kann oder nicht.

OnesimusUnbound
quelle
Danke für die Antwort. Die sqrt () war nur ein Beispiel, dasselbe Verhalten mit Eingabeparametern kann von vielen anderen Funktionen verwendet werden. "Wenn die Funktion so aktualisiert wird, dass die Validierung des Parameters beeinträchtigt wird, müssen Sie nach jedem Auftreten der Anrufervalidierung suchen" - ich bin damit nicht einverstanden. Wir können dann dasselbe für den Rückgabewert sagen: Wenn die Funktion so aktualisiert wird, dass sie den Rückgabewert beeinflusst, müssen Sie jeden Aufrufer korrigieren der anruferwechsel ist sowieso notwendig.
Srnka
2

Eine Funktion sollte ihre Vor- und Nachbedingungen angeben.
Die Vorbedingungen sind die Bedingungen, die vom Aufrufer erfüllt werden müssen, bevor er die Funktion korrekt verwenden kann und die Gültigkeit von Eingabeparametern einschließen kann (und dies häufig auch tut).
Die Nachbedingungen sind die Versprechungen, die die Funktion ihren Anrufern macht.

Wenn die Gültigkeit der Parameter einer Funktion Teil der Vorbedingungen ist, muss der Aufrufer sicherstellen, dass diese Parameter gültig sind. Dies bedeutet jedoch nicht, dass jeder Aufrufer jeden Parameter vor dem Aufruf explizit überprüfen muss. In den meisten Fällen sind keine expliziten Tests erforderlich, da die interne Logik und die Bedingungen des Aufrufers bereits sicherstellen, dass die Parameter gültig sind.

Als Sicherheitsmaßnahme gegen Programmierfehler (Bugs) können Sie überprüfen, ob die an eine Funktion übergebenen Parameter tatsächlich die angegebenen Voraussetzungen erfüllen. Da diese Tests kostspielig sein können, ist es eine gute Idee, sie für Release-Builds deaktivieren zu können. Wenn diese Tests fehlschlagen, sollte das Programm beendet werden, da nachweislich ein Fehler aufgetreten ist.

Obwohl auf den ersten Blick das Einchecken des Anrufers zu doppelten Codes zu führen scheint, ist es tatsächlich umgekehrt. Das Einchecken des Angerufenen führt zu doppelten Codes und vielen unnötigen Arbeiten.
Denken Sie nur daran, wie oft Sie Parameter durch mehrere Funktionsebenen führen und dabei nur geringfügige Änderungen an einigen von ihnen vornehmen. Wenn Sie die Check-in-Callee- Methode konsequent anwenden , muss jede dieser Zwischenfunktionen die Prüfung für jeden der Parameter erneut durchführen.
Und nun stellen Sie sich vor, dass einer dieser Parameter eine sortierte Liste sein soll.
Beim Einchecken des Anrufers müsste nur die erste Funktion sicherstellen, dass die Liste wirklich sortiert ist. Alle anderen wissen, dass die Liste bereits sortiert ist (wie sie in ihrer Vorbedingung angegeben haben) und können sie ohne weitere Überprüfung weiterleiten.

Bart van Ingen Schenau
quelle
+1 Danke für die Antwort. Nettes Spiegelbild: "Das Einchecken des Angerufenen führt zu doppelten Codes und vielen unnötigen Arbeiten." Und im Satz: "In den meisten Fällen sind keine expliziten Tests erforderlich, da die interne Logik und die Voraussetzungen des Aufrufers bereits dafür sorgen" - was meinen Sie mit dem Ausdruck "interne Logik"? Die DBC-Funktionalität?
Srnka
@srnka: Mit "interner Logik" meine ich die Berechnungen und Entscheidungen in einer Funktion. Es ist im Wesentlichen die Implementierung der Funktion.
Bart van Ingen Schenau
0

Meistens können Sie nicht wissen, wer, wann und wie die von Ihnen geschriebene Funktion aufruft. Es ist am besten, das Schlimmste anzunehmen: Ihre Funktion wird mit ungültigen Parametern aufgerufen. Darum sollten Sie sich unbedingt kümmern.

Wenn die von Ihnen verwendete Sprache Ausnahmen unterstützt, prüfen Sie möglicherweise nicht auf bestimmte Fehler und stellen sicher, dass eine Ausnahme ausgelöst wird. In diesem Fall müssen Sie den Fall jedoch in der Dokumentation beschreiben (Dokumentation erforderlich). Die Ausnahme gibt dem Aufrufer genügend Informationen darüber, was passiert ist, und lenkt die Aufmerksamkeit auf die ungültigen Argumente.

superM
quelle
Tatsächlich ist es möglicherweise besser, den Parameter zu validieren und, falls der Parameter ungültig ist, selbst eine Ausnahme auszulösen. Hier ist der Grund: Die Clowns, die Ihre Routine aufrufen, ohne sich darum zu kümmern, ob sie gültige Daten angegeben haben, sind dieselben, die sich nicht darum kümmern, den Fehlerrückgabecode zu überprüfen, der angibt, dass sie ungültige Daten übergeben haben. Wenn Sie eine Ausnahme auslösen, wird das zu behebende Problem ERZWUNGEN.
John R. Strohm