So führen Sie eine Eingabevalidierung ohne Ausnahmen oder Redundanz durch

11

Wenn ich versuche, eine Schnittstelle für ein bestimmtes Programm zu erstellen, versuche ich im Allgemeinen, Ausnahmen zu vermeiden, die von nicht validierten Eingaben abhängen.

Was also oft passiert, ist, dass ich an einen solchen Code gedacht habe (dies ist nur ein Beispiel für ein Beispiel, egal welche Funktion er ausführt, Beispiel in Java):

public static String padToEvenOriginal(int evenSize, String string) {
    if (evenSize % 2 == 1) {
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, sagen wir also, dass dies evenSizetatsächlich aus Benutzereingaben abgeleitet wird. Ich bin mir also nicht sicher, ob es gerade ist. Ich möchte diese Methode jedoch nicht mit der Möglichkeit aufrufen, dass eine Ausnahme ausgelöst wird. Also mache ich folgende Funktion:

public static boolean isEven(int evenSize) {
    return evenSize % 2 == 0;
}

Jetzt habe ich zwei Überprüfungen, die dieselbe Eingabevalidierung durchführen: den Ausdruck in der ifAnweisung und das explizite Einchecken isEven. Doppelter Code, nicht schön, also lasst uns umgestalten:

public static String padToEvenWithIsEven(int evenSize, String string) {
    if (!isEven(evenSize)) { // to avoid duplicate code
        throw new IllegalArgumentException("evenSize argument is not even");
    }

    if (string.length() >= evenSize) {
        return string;
    }

    StringBuilder sb = new StringBuilder(evenSize);
    sb.append(string);
    for (int i = string.length(); i < evenSize; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

OK, das hat es gelöst, aber jetzt kommen wir in die folgende Situation:

String test = "123";
int size;
do {
    size = getSizeFromInput();
} while (!isEven(size)); // checks if it is even
String evenTest = padToEvenWithIsEven(size, test);
System.out.println(evenTest); // checks if it is even (redundant)

Jetzt haben wir eine redundante Prüfung: Wir wissen bereits, dass der Wert gerade ist, führen aber padToEvenWithIsEvendennoch die Parameterprüfung durch, die immer true zurückgibt, wie wir diese Funktion bereits aufgerufen haben.

Das ist isEvennatürlich kein Problem, aber wenn die Parameterprüfung umständlicher ist, kann dies zu hohe Kosten verursachen. Außerdem fühlt sich ein redundanter Anruf einfach nicht richtig an.

Manchmal können wir dies umgehen, indem wir einen "validierten Typ" einführen oder eine Funktion erstellen, bei der dieses Problem nicht auftreten kann:

public static String padToEvenSmarter(int numberOfBigrams, String string) {
    int size = numberOfBigrams * 2;
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append('x');
    }
    return sb.toString();
}

Dies erfordert jedoch ein kluges Denken und einen ziemlich großen Refaktor.

Gibt es eine (allgemeinere) Methode, mit der wir redundante Aufrufe isEvenund doppelte Parameterprüfungen vermeiden und durchführen können? Ich möchte, dass die Lösung nicht padToEvenmit einem ungültigen Parameter aufruft und die Ausnahme auslöst.


Ohne Ausnahmen meine ich keine ausnahmefreie Programmierung, ich meine, dass Benutzereingaben keine Ausnahme von Natur aus auslösen, während die generische Funktion selbst noch die Parameterprüfung enthält (wenn auch nur zum Schutz vor Programmierfehlern).

Maarten Bodewes
quelle
Versuchen Sie nur, die Ausnahme zu entfernen? Sie können dies tun, indem Sie eine Annahme über die tatsächliche Größe treffen. Wenn Sie beispielsweise 13 übergeben, füllen Sie entweder 12 oder 14 auf und vermeiden Sie die Prüfung insgesamt. Wenn Sie eine dieser Annahmen nicht treffen können, bleibt die Ausnahme bestehen, da der Parameter unbrauchbar ist und Ihre Funktion nicht fortgesetzt werden kann.
Robert Harvey
@ RobertHarvey Das widerspricht meiner Meinung nach direkt dem Prinzip der geringsten Überraschung sowie dem Prinzip des schnellen Scheiterns. Es ist fast so, als würde man null zurückgeben, wenn der Eingabeparameter null ist (und dann vergessen, das Ergebnis korrekt zu behandeln, natürlich unerklärliche NPE).
Maarten Bodewes
Ergo die Ausnahme. Richtig?
Robert Harvey
2
Warte was? Du bist hierher gekommen, um Rat zu bekommen, oder? Was ich sagen will (in meinem anscheinend nicht so subtile Weise), ist , dass diese Ausnahmen sind durch Design, also , wenn Sie sie beseitigen wollen, sollten Sie wahrscheinlich einen guten Grund haben. Übrigens stimme ich amon zu: Sie sollten wahrscheinlich keine Funktion haben, die keine ungeraden Zahlen für einen Parameter akzeptieren kann.
Robert Harvey
1
@ MaartenBodewes: Sie müssen sich daran erinnern, dass padToEvenWithIsEven keine Überprüfung der Benutzereingaben durchgeführt wird. Es führt eine Gültigkeitsprüfung seiner Eingabe durch, um sich vor Programmierfehlern im aufrufenden Code zu schützen . Wie umfangreich diese Validierung sein muss, hängt von einer Kosten- / Risikoanalyse ab, bei der Sie die Kosten der Prüfung gegen das Risiko stellen, dass die Person, die den aufrufenden Code schreibt, den falschen Parameter übergibt.
Bart van Ingen Schenau

Antworten:

7

In Ihrem Beispiel besteht die beste Lösung darin, eine allgemeinere Auffüllfunktion zu verwenden. Wenn der Anrufer eine gerade Größe auffüllen möchte, kann er dies selbst überprüfen.

public static String padString(int size, String string) {
    if (string.length() >= size) {
        return string;
    }

    StringBuilder sb = new StringBuilder(size);
    sb.append(string);
    for (int i = string.length(); i < size; i++) {
        sb.append(' ');
    }
    return sb.toString();
}

Wenn Sie wiederholt dieselbe Validierung für einen Wert durchführen oder nur eine Teilmenge von Werten eines Typs zulassen möchten, können Mikrotypen / winzige Typen hilfreich sein. Für allgemeine Dienstprogramme wie das Auffüllen ist dies keine gute Idee. Wenn Ihr Wert jedoch eine bestimmte Rolle in Ihrem Domänenmodell spielt, kann die Verwendung eines dedizierten Typs anstelle primitiver Werte ein großer Fortschritt sein. Hier könnten Sie definieren:

final class EvenInteger {
  public final int value;

  public EvenInteger(int value) {
    if (!(value % 2 == 0))
      throw new IllegalArgumentException("EvenInteger(" + value + ") is not even");
    this.value = value;
  }
}

Jetzt können Sie deklarieren

public static String padStringToEven(EvenInteger evenSize, String string)
    ...

und müssen keine interne Validierung durchführen. Für einen so einfachen Test ist das Einwickeln eines Int in ein Objekt im Hinblick auf die Laufzeitleistung wahrscheinlich teurer. Die Verwendung des Typsystems zu Ihrem Vorteil kann jedoch Fehler reduzieren und Ihr Design klarstellen.

Die Verwendung derart winziger Typen kann sogar dann nützlich sein, wenn sie keine Validierung durchführen, z. B. um eine Zeichenfolge, die a darstellt, FirstNamevon a zu unterscheiden LastName. Ich verwende dieses Muster häufig in statisch typisierten Sprachen.

amon
quelle
Hat Ihre zweite Funktion in einigen Fällen immer noch keine Ausnahme ausgelöst?
Robert Harvey
1
@RobertHarvey Ja, zum Beispiel bei Nullzeigern. Aber jetzt wird die Hauptprüfung - dass die Nummer gerade ist - aus der Funktion in die Verantwortung des Anrufers gezwungen. Sie können dann mit der Ausnahme auf eine Weise umgehen, die sie für richtig halten. Ich denke, die Antwort konzentriert sich weniger darauf, alle Ausnahmen zu beseitigen, als darauf, die Codeduplizierung im Validierungscode zu beseitigen.
Amon
1
Gute Antwort. Natürlich ist die Strafe für das Erstellen von Datenklassen in Java ziemlich hoch (viel zusätzlicher Code), aber das ist eher ein Problem der Sprache als der Idee, die Sie vorgestellt haben. Ich denke, dass Sie diese Lösung nicht für alle Anwendungsfälle verwenden würden, in denen mein Problem auftreten würde, aber es ist eine gute Lösung gegen übermäßige Verwendung von Grundelementen oder für Randfälle, in denen die Kosten für die Parameterprüfung außergewöhnlich hoch sind.
Maarten Bodewes
1
@ MaartenBodewes Wenn Sie eine Validierung durchführen möchten, können Sie so etwas wie Ausnahmen nicht loswerden. Die Alternative besteht darin, eine statische Funktion zu verwenden, die bei einem Fehler ein validiertes Objekt oder null zurückgibt, aber im Grunde keine Vorteile hat. Indem wir die Validierung aus der Funktion in den Konstruktor verschieben, können wir die Funktion jetzt ohne die Möglichkeit eines Validierungsfehlers aufrufen. Das gibt dem Anrufer die Kontrolle. ZB:EvenInteger size; while (size == null) { try { size = new EvenInteger(getSizeFromInput()); } catch(...){}} String result = padStringToEven(size,...);
Amon
1
@ MaartenBodewes Die doppelte Validierung erfolgt ständig. Bevor Sie beispielsweise versuchen, eine Tür zu schließen, können Sie prüfen, ob sie bereits geschlossen ist. Die Tür selbst kann jedoch auch nicht wieder geschlossen werden, wenn sie bereits geschlossen ist. Es gibt einfach keinen Ausweg, außer wenn Sie immer darauf vertrauen, dass der Anrufer keine ungültigen Vorgänge ausführt. In Ihrem Fall haben Sie möglicherweise eine unsafePadStringToEvenOperation, die keine Überprüfungen durchführt, aber es scheint eine schlechte Idee zu sein, nur um eine Validierung zu vermeiden.
Plalx
9

Als Erweiterung von @ amons Antwort können wir seine EvenIntegermit dem kombinieren, was die funktionale Programmiergemeinschaft als "Smart Constructor" bezeichnen könnte - eine Funktion, die den dummen Konstruktor umschließt und sicherstellt, dass sich das Objekt in einem gültigen Zustand befindet (wir machen den dummen Konstruktorklasse oder in nicht klassenbasierten Sprachen (Modul / Paket privat, um sicherzustellen, dass nur die intelligenten Konstruktoren verwendet werden). Der Trick besteht darin, ein Optional(oder ein Äquivalent) zurückzugeben, um die Funktion komponierbarer zu machen.

public final class EvenInteger {
    private final int value;

    private EvenInteger(value) {
        this.value = value;
    }

    public static Optional<EvenInteger> of(final int value) {
        if (value % 2 == 0) {
            return Optional.of(new EvenInteger(value));
        }
        return Optional.empty();
    }

    public int getValue() {
        return this.value;
    }
}

Wir können dann leicht Standardmethoden Optionalverwenden, um die Eingabelogik zu schreiben:

class GetEvenInput {
    public Optional<EvenInteger> askOnce() {
        int size = getSizeFromInput();
        return EvenInteger.of(size);
    }

    public EvenInteger keepAsking() {
        return askOnce().orElseGet(() -> keepAsking());
    }
}

Sie können auch keepAsking()in einem idiomatischeren Java-Stil mit einer Do-While-Schleife schreiben .

Optional<EvenInteger> result;
do {
    result = askOnce();
} while (!result.isPresent());

return result.get();

Dann können Sie sich im Rest Ihres Codes EvenIntegermit Sicherheit darauf verlassen, dass es wirklich gleichmäßig sein wird und unser gerader Scheck nur einmal in geschrieben wurde EvenInteger::of.

Walpen
quelle
Optional repräsentiert im Allgemeinen potenziell ungültige Daten recht gut. Dies ist analog zu einer Vielzahl von TryParse-Methoden, die sowohl Daten als auch ein Flag zurückgeben, das die Gültigkeit der Eingabe anzeigt. Mit konformen Zeitprüfungen.
Basilevs
1

Eine zweimalige Validierung ist ein Problem, wenn das Ergebnis der Validierung dasselbe ist UND die Validierung in derselben Klasse durchgeführt wird. Das ist nicht dein Beispiel. In Ihrem überarbeiteten Code ist die erste isEven-Prüfung eine Eingabevalidierung. Ein Fehler führt dazu, dass eine neue Eingabe angefordert wird. Die zweite Prüfung ist völlig unabhängig von der ersten, da es sich um eine öffentliche Methode padToEvenWithEven handelt, die von außerhalb der Klasse aufgerufen werden kann und ein anderes Ergebnis hat (die Ausnahme).

Ihr Problem ähnelt dem Problem, dass versehentlich identischer Code für nicht trocken verwechselt wird. Sie verwechseln die Implementierung mit dem Design. Sie sind nicht gleich, und nur weil Sie ein oder ein Dutzend Zeilen haben, die gleich sind, bedeutet dies nicht, dass sie denselben Zweck erfüllen und für immer als austauschbar angesehen werden können. Wahrscheinlich lässt du deine Klasse auch zu viel tun, aber überspringe das, da dies wahrscheinlich nur ein Spielzeugbeispiel ist ...

Wenn dies ein Leistungsproblem ist, können Sie das Problem lösen, indem Sie eine private Methode erstellen, die keine Validierung durchführt, die Ihr öffentliches padToEvenWithEven nach der Validierung aufgerufen hat und die Ihre andere Methode stattdessen aufruft. Wenn es sich nicht um ein Leistungsproblem handelt, lassen Sie Ihre verschiedenen Methoden die Überprüfungen durchführen, die sie zur Ausführung der ihnen zugewiesenen Aufgaben benötigen.

jmoreno
quelle
OP gab an, dass die Eingabevalidierung durchgeführt wird, um das Auslösen der Funktion zu verhindern. Die Prüfungen sind also völlig abhängig und absichtlich gleich.
D Drmmr
@DDrmmr: Nein, sie sind nicht abhängig. Die Funktion wird ausgelöst, da dies Teil des Vertrags ist. Wie gesagt, die Antwort ist, eine private Methode zu erstellen, die alles außer der Ausnahme auslöst und dann die öffentliche Methode die private Methode aufrufen lässt. Die öffentliche Methode behält die Prüfung bei, wo sie einem anderen Zweck dient - der Verarbeitung nicht validierter Eingaben.
jmoreno