Validation des données: classe séparée ou non?

16

Lorsque j'ai beaucoup de données à valider, dois-je créer une nouvelle classe dans le seul but de la validation ou dois-je m'en tenir à la validation dans la méthode?

Mon exemple particulier envisage un tournoi et une classe d'événement / catégorie: Tournamentet Event, qui modélise un tournoi sportif et chaque tournoi a une ou plusieurs catégories.

Il y a toutes sortes de choses à valider dans ces classes: les joueurs doivent être vides, doivent être uniques, le nombre de matchs que chaque joueur doit jouer, le nombre de joueurs que chaque match a, des matchs prédéfinis, et un très gros etcetera, y compris beaucoup plus règles complexes.

Il y a aussi certaines parties que je dois valider dans son ensemble, comme la façon dont les classes s'intègrent les unes aux autres. Par exemple, la validation unitaire d'un Playerpeut être très bien, mais si un événement a le même joueur deux fois, c'est une erreur de validation.

Alors qu'en est-il?: J'oublie absolument toute pré-vérification lorsque j'utilise les setters de mes classes de modèles et des méthodes similaires pour ajouter des données et à la place je laisse les classes de validation s'en occuper.

Nous aurons donc quelque chose comme EventValidatoravec un en Eventtant que membre et une validate()méthode qui valide tout l'objet, ainsi que des méthodes singulières pour valider les règles de tous les membres.

Ensuite, avant d'instancier un objet validable, j'exécuterai la validation pour empêcher les valeurs illégales.

Ma conception est-elle correcte? Dois-je faire quelque chose différemment?

De plus, dois-je utiliser des méthodes de validation de retour booléen? Ou simplement lever une exception si la validation échoue? Il me semble que la meilleure option serait de renvoyer des méthodes booléennes et de lever l'exception lorsque l'objet est instancié, par exemple:

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
la source

Réponses:

7

Il est acceptable de déléguer toute logique au moyen de la composition si cette logique va changer dynamiquement pendant l'exécution d'un programme. Les validations complexes comme celles que vous expliquez sont aussi bonnes que n'importe quelle candidate à déléguer à une autre classe via la composition.

N'oubliez pas cependant que les validations peuvent se produire à différents moments.

Instancier un validateur concret comme dans votre exemple est une mauvaise idée car il couple votre classe d'événement à ce validateur particulier.

Supposons que vous n'utilisez aucun framework DI.

Vous pouvez ajouter le validateur au constructeur ou l'injecter avec une méthode setter. Je suggère qu'une méthode de créateur dans une usine instancie à la fois l'événement et le validateur, puis la transmet soit au constructeur de l'événement, soit avec une méthode setValidator.

De toute évidence, une interface Validator et / ou une classe abstraite doivent être écrites afin que vos classes en dépendent et non d'un validateur concret.

L'exécution de la méthode de validation dans le constructeur peut être problématique car tout l'état que vous souhaitez valider n'est peut-être pas encore en place.

Je vous suggère de créer une interface validable et de laisser vos classes l'implémenter, cette interface pourrait avoir une méthode validate ().

De cette façon, les composants supérieurs de votre application appellent la méthode de validation à volonté (qui à son tour est déléguée au membre validateur).

==> 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
la source
Donc, en termes de hiérarchie des classes, cela ne diffère pas beaucoup de ce que j'ai suggéré, n'est-ce pas? Au lieu d'instancier et d'appeler le validateur à l'intérieur du constructeur, il serait créé et appelé en cas de besoin dans le flux d'exécution, c'est la principale différence que je peux voir.
dabadaba
Ma réponse est essentiellement qu'il est OK de créer une autre classe pour effectuer une validation complexe. Je viens d'ajouter quelques conseils pour éviter les couplages durs et obtenir plus de flexibilité.
Tulains Córdova
Si je devais ajouter des messages d'erreur dans une liste ou une dictée, devrait-il être dans Validatorou dans Validable? Et comment puis-je travailler ces messages en conjonction avec le ValidationException?
dabadaba
1
@dabadaba La liste doit être membre des implémenteurs IValidator, mais IValidable doit ajouter l'accès à cette méthode afin que les implémenteurs la délèguent. J'ai supposé que plusieurs messages de validation pouvaient être retournés. Cliquez sur le lien modifié il y a n minutes en bas pour voir les différences. C'est mieux si vous utilisez la vue côte à côte (pas de démarque).
Tulains Córdova
3
Être probablement pédant, mais je suppose que Validatablec'est un bien meilleur nom que Validablecomme c'est le vrai adjectif
user919426
4

J'oublie absolument toute pré-vérification lors de l'utilisation des setters de mes classes de modèles et des méthodes similaires pour ajouter des données

C'est le problème. Idéalement, vous devez empêcher que vos objets aient un état non valide: n'autorisez pas l'instanciation avec un état non valide et si vous devez avoir des setters et d'autres méthodes de changement d'état, lancez une exception juste là.

à la place, je laisse les classes de validation gérer la validation.

Ce n'est pas contradictoire. Si la logique de validation est suffisamment complexe pour justifier sa propre classe, vous pouvez toujours déléguer la validation. Et si je comprends bien, c'est exactement ce que vous faites maintenant:

Ensuite, avant d'instancier un objet validable, j'exécuterai la validation pour empêcher les valeurs illégales.

Si vous avez encore des setters qui pourraient entraîner un état non valide, assurez-vous de valider avant de réellement changer d'état. Sinon, vous aurez un message d'erreur, mais vous laisserez toujours votre objet avec un état non valide. Encore une fois: empêchez vos objets d'avoir un état invalide

De plus, dois-je utiliser des méthodes de validation de retour booléen? Ou simplement lever une exception si la validation échoue? Il me semble que la meilleure option serait de renvoyer des méthodes booléennes et de lever l'exception lorsque l'objet est instancié

Cela me semble bien, car le validateur attend une entrée valide ou invalide, donc de son point de vue, une entrée invalide ne fait pas exception.

Mise à jour: si "le système dans son ensemble" devient invalide, la raison en est qu'un objet doit avoir changé (par exemple un joueur) et un objet éventuellement de niveau supérieur (par exemple un événement) qui fait référence à cet objet a une condition qui n'est plus remplie . Désormais, une solution serait de ne pas autoriser de modifications directes du lecteur qui pourraient rendre quelque chose non valide à un niveau supérieur. C'est là que les objets immuables aident. Un joueur changé est alors un objet différent. Une fois qu'un joueur est associé à un événement, vous ne pouvez le modifier qu'à partir de l'événement lui-même (car vous devrez changer la référence à un nouvel objet) et valider à nouveau immédiatement.

Fabian Schmengler
la source
Qu'en est-il de n'avoir aucun contrôle que ce soit dans l'instanciation et les setters et d'avoir la validation en tant qu'opération apart? Quelque chose comme Tulains Córdova l'a suggéré. Parce que valider et lever une exception dans un setter ou un constructeur peut fonctionner pour ce membre ou cet objet, mais cela n'aide pas à vérifier si le système dans son ensemble est correct.
dabadaba
@dabadaba ma réponse était trop longue pour un commentaire, veuillez voir la mise à jour ci
Fabian Schmengler
Je ne veux pas utiliser d'objets immuables, je veux pouvoir modifier mes classes. Et je ne sais pas comment créer des objets immuables, sauf si vous faites référence au mot-clé final. Dans ce cas, je devrais beaucoup changer ma conception car je construis partiellement des événements et des tournois avec des setters supplémentaires (car ces données sont supplémentaires et configurables).
dabadaba
3
Pour créer des objets immuables, supprimez tous les setters et utilisez uniquement des constructeurs pour définir des valeurs. Si vous avez des données facultatives ou des données qui ne sont pas disponibles immédiatement, pensez à utiliser le modèle de générateur: developer.amd.com/community/blog/2009/02/06/…
Fabian Schmengler