Datenvalidierung: getrennte Klasse oder nicht?

15

Wenn ich viele Daten habe, die validiert werden müssen, sollte ich eine neue Klasse zum alleinigen Zweck der Validierung erstellen oder mich an die methodeninterne Validierung halten?

Mein spezielles Beispiel sieht ein Turnier und eine Event- / Kategorie-Klasse vor: Tournamentund Event, die ein Sportturnier modelliert und jedes Turnier eine oder mehrere Kategorien hat.

In diesen Klassen gilt es alle möglichen Dinge zu validieren: Die Spieler sollten leer sein, einzigartig, die Anzahl der Matches, die jeder Spieler spielen sollte, die Anzahl der Spieler, die jedes Match hat, vordefinierte Matchups und ein wirklich großes usw., einschließlich viel mehr komplexe Regeln.

Es gibt auch einige Teile, die ich als Ganzes überprüfen muss, z. B. wie sich Klassen ineinander integrieren. Zum Beispiel kann die einheitliche Validierung von a Playerin Ordnung sein, aber wenn ein Event zweimal denselben Spieler hat, ist das ein Validierungsfehler.

Wie wäre es damit ?: Ich vergesse jegliche Vorabprüfung, wenn ich die Setter meiner Modellklassen und ähnliche Methoden zum Hinzufügen von Daten verwende, und lasse stattdessen Validierungsklassen damit umgehen.

Wir werden also so etwas wie EventValidatoreine Eventals Mitglied haben und eine validate()Methode, die das gesamte Objekt validiert, sowie singuläre Methoden, um die Regeln aller Mitglieder zu validieren.

Bevor ich dann ein validierbares Objekt instanziiere, werde ich die Validierung durchführen, um unzulässige Werte zu verhindern.

Ist mein Design korrekt? Sollte ich etwas anders machen?

Sollte ich auch boolesche Rückgabevalidierungsmethoden verwenden? Oder einfach eine Ausnahme auslösen, wenn die Validierung fehlschlägt? Es scheint mir, dass die beste Option boolesche Rückgabemethoden sind und die Ausnahme auslösen, wenn das Objekt instanziiert ist, zum Beispiel:

public Event() {
    EventValidator eventValidator = new EventValidator(this);
    if (!eventValidator.validate()) {
        // show error messages with methods defined in the validator
        throw new Exception(); // what type of exception would be best? should I create custom ones?
    }
}
Dabadaba
quelle

Antworten:

7

Es ist in Ordnung, eine Logik durch Komposition zu delegieren, wenn sich diese Logik während der Ausführung eines Programms dynamisch ändert. Komplexe Validierungen wie die, die Sie erklären, sind genauso gut geeignet, um durch Komposition an eine andere Klasse delegiert zu werden.

Denken Sie jedoch daran, dass Überprüfungen zu unterschiedlichen Zeitpunkten erfolgen können.

Das Instanziieren eines konkreten Validators wie in Ihrem Beispiel ist eine schlechte Idee, da dadurch Ihre Event-Klasse mit diesem bestimmten Validator gekoppelt wird.

Angenommen, Sie verwenden kein DI-Framework.

Sie können den Validator dem Konstruktor hinzufügen oder ihm eine Setter-Methode hinzufügen. Ich schlage vor, dass eine Erstellermethode in einer Factory sowohl das Ereignis als auch den Validator instanziiert und dann entweder an den Ereigniskonstruktor oder mit einer setValidator-Methode übergibt.

Offensichtlich sollte eine Validator-Schnittstelle und / oder eine abstrakte Klasse geschrieben werden, damit Ihre Klassen davon abhängen und nicht von einem konkreten Validator.

Das Ausführen der validate-Methode im Konstruktor kann problematisch sein, da möglicherweise noch nicht alle zu validierenden Zustände vorhanden sind.

Ich schlage vor, dass Sie ein Validable-Interface erstellen und Ihre Klassen es implementieren lassen. Dieses Interface könnte eine validate () -Methode haben.

Auf diese Weise rufen die oberen Komponenten Ihrer Anwendung die Validierungsmethode nach Belieben auf (die wiederum an das Validierungsmitglied delegiert wird).

==> IValidable.java <==

import java.util.List;

public interface IValidable {
    public void setValidator(IValidator<Event> validator_);
    public void validate() throws ValidationException;
    public List<String> getMessages();
}

==> IValidator.java <==

import java.util.List;

public interface IValidator<T> {
    public boolean validate(T e);
    public List<String> getValidationMessages();
}

==> Event.java <==

import java.util.List;

public class Event implements IValidable {

    private IValidator<Event> validator;

    @Override
    public void setValidator(IValidator<Event> validator_) {
        this.validator = validator_;
    }

    @Override
    public void validate() throws ValidationException {
        if (!this.validator.validate(this)){
            throw new ValidationException("WTF!");
        }
    }

    @Override
    public List<String> getMessages() {
        return this.validator.getValidationMessages();
    }

}

==> SimpleEventValidator.java <==

import java.util.ArrayList;
import java.util.List;

public class SimpleEventValidator implements IValidator<Event> {

    private List<String> messages = new ArrayList<String>();
    @Override
    public boolean validate(Event e) {
        // do validations here, by accessing the public getters of e
        // propulate list of messages is necessary
        // this example always returns false    
        return false;
    }

    @Override
    public List<String> getValidationMessages() {
        return this.messages;
    }

}

==> ValidationException.java <==

public class ValidationException extends Exception {
    public ValidationException(String message) {
        super(message);
    }

    private static final long serialVersionUID = 1L;
}

==> Test.java <==

public class Test {
    public static void main (String args[]){
        Event e = new Event();
        IValidator<Event> v = new SimpleEventValidator();
        e.setValidator(v);
        // set other thins to e like
        // e.setPlayers(player1,player2,player3)
        // e.setNumberOfMatches(3);
        // etc
        try {
            e.validate();
        } catch (ValidationException e1) {
            System.out.println("Your event doesn't comply with the federation regulations for the following reasons: ");
            for (String s: e.getMessages()){
                System.out.println(s);
            }
        }
    }
}
Tulains Córdova
quelle
In Bezug auf die Klassenhierarchie unterscheidet sich das also nicht sehr von dem, was ich vorgeschlagen habe, oder? Anstatt den Validator innerhalb des Konstruktors zu instanziieren und aufzurufen, würde er erstellt und bei Bedarf im Ausführungsablauf aufgerufen. Dies ist der Hauptunterschied, den ich sehe.
dabadaba
Meine Antwort ist im Grunde genommen, dass es in Ordnung ist, eine weitere Klasse für die komplexe Validierung zu erstellen. Ich habe nur einige Ratschläge für Sie hinzugefügt, um eine harte Kopplung zu vermeiden und mehr Flexibilität zu erhalten.
Tulains Córdova
Wenn ich einer Liste oder einem Wörterbuch Fehlermeldungen hinzufügen würde, sollte es in Validatoroder in sein Validable? Und wie kann ich diese Nachrichten in Verbindung mit dem bearbeiten ValidationException?
Dabadaba
1
@dabadaba Die Liste sollte ein Mitglied der IValidator-Implementierer sein, IValidable sollte jedoch den Zugriff auf diese Methode hinzufügen, damit die Implementierer sie delegieren können. Ich bin davon ausgegangen, dass mehr als eine Bestätigungsnachricht zurückgegeben werden kann. Klicken Sie unten auf den Link " Vor n Minuten bearbeitet", damit Sie die Unterschiede sehen können. Es ist besser, wenn Sie die Side-by-Side- Ansicht verwenden (kein Abschlag).
Tulains Córdova
3
Wahrscheinlich pedantisch zu sein, aber ich gehe davon Validatableist ein weit besserer Name als Validablewie es das reale Adjektiv ist
user919426
4

Ich vergesse jede Vorabprüfung, wenn ich die Setter meiner Modellklassen und ähnliche Methoden zum Hinzufügen von Daten verwende

Das ist das Problem. Idealerweise sollten Sie verhindern, dass Ihre Objekte einen ungültigen Status haben: Lassen Sie keine Instanziierung mit ungültigem Status zu, und wenn Sie Setter und andere Methoden zum Ändern des Status benötigen, lösen Sie genau dort eine Ausnahme aus.

Stattdessen überlasse ich Validierungsklassen die Validierung.

Dies ist kein Widerspruch. Wenn die Validierungslogik komplex genug ist, um eine eigene Klasse zu rechtfertigen, können Sie die Validierung dennoch delegieren. Und wenn ich Sie richtig verstehe, ist dies genau das, was Sie jetzt tun:

Bevor ich dann ein validierbares Objekt instanziiere, werde ich die Validierung durchführen, um unzulässige Werte zu verhindern.

Wenn Sie immer noch Setter haben, die möglicherweise zu einem ungültigen Status führen, stellen Sie sicher, dass Sie validieren, bevor Sie den Status tatsächlich ändern. Andernfalls wird eine Fehlermeldung angezeigt, und das Objekt weist weiterhin einen ungültigen Status auf. Nochmals: Verhindern Sie, dass Ihre Objekte einen ungültigen Status haben

Sollte ich auch boolesche Rückgabevalidierungsmethoden verwenden? Oder einfach eine Ausnahme auslösen, wenn die Validierung fehlschlägt? Mir scheint, die beste Option wären boolesche Rückgabemethoden und die Ausnahmebedingung, wenn das Objekt instanziiert wird

Hört sich für mich gut an, da der Prüfer eine gültige oder ungültige Eingabe erwartet, ist eine ungültige Eingabe aus seiner Sicht keine Ausnahme.

Update: Wenn "das System als Ganzes" ungültig wird, muss sich ein Objekt geändert haben (z. B. ein Spieler) und ein möglicherweise übergeordnetes Objekt (z. B. ein Ereignis), das auf dieses Objekt verweist, hat eine Bedingung, die nicht mehr erfüllt ist . Eine Lösung wäre nun, keine direkten Änderungen am Player zuzulassen, die auf einer höheren Ebene zu einer Ungültigkeit führen könnten. Hier helfen unveränderliche Objekte . Dann ist ein veränderter Spieler ein anderes Objekt. Sobald ein Spieler einem Ereignis zugeordnet ist, können Sie es nur vom Ereignis selbst aus ändern (da Sie den Verweis auf ein neues Objekt ändern müssen) und es sofort erneut validieren.

Fabian Schmengler
quelle
Was ist, wenn die Instanziierung und die Setter überhaupt nicht überprüft werden und die Validierung als Nebenoperation zur Verfügung steht? So etwas wie Tulains Córdova. Da das Validieren und Auslösen einer Ausnahme in einem Setter oder Konstruktor möglicherweise für dieses Member oder Objekt funktioniert, hilft es nicht, zu überprüfen, ob das System als Ganzes in Ordnung ist.
Dabadaba
@ Dabadaba meine Antwort war zu lang für einen Kommentar, siehe Update oben
Fabian Schmengler
Ich möchte jedoch keine unveränderlichen Objekte verwenden, sondern meine Klassen ändern können. Und ich weiß nicht, wie man unveränderliche Objekte erstellt, es sei denn, Sie beziehen sich auf das endgültige Schlüsselwort. In diesem Fall müsste ich mein Design sehr ändern, da ich teilweise Events und Turniere mit zusätzlichen Setzern konstruiere (da diese Daten zusätzlich und konfigurierbar sind).
Dabadaba
3
Um unveränderliche Objekte zu erstellen, entfernen Sie alle Setter und verwenden Sie nur Konstruktoren, um Werte festzulegen. Wenn Sie optionale Daten oder Daten haben, die nicht sofort verfügbar sind, können Sie das Builder-Muster verwenden: developer.amd.com/community/blog/2009/02/06/…
Fabian Schmengler