Refactoring du système de validation
J'ai un système de validation qui valide les données d'entrée avant de les enregistrer dans la base de données. Alors disons que je veux créer un nouvel utilisateur. Nous sommes dans la classe de service:
package main.user;
import main.entity.User;
import main.user.validator.attributesvalidators.UserAttributesValidator;
import main.user.validator.availabilityvalidators.UserAvailabilityValidator;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserCrudActivitiesService {
private final UserRepository userRepository;
public UserCrudActivitiesService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public List<String> createUser(User user) {
UserAttributesValidator userAttributesValidator = new UserAttributesValidator();
UserAvailabilityValidator userAvailabilityValidator = new UserAvailabilityValidator(userRepository);
List<String> messages = userAttributesValidator.validate(user);
messages.addAll(userAvailabilityValidator.check(user));
if (messages.isEmpty()) {
userRepository.save(user);
//TODO passwordencoder
}
return messages;
}
public User updateUser(User user) {
return userRepository.save(user);
}
}
Nous avons deux validateurs - d'abord qui vérifieront si les attributs de l'utilisateur sont corrects et ensuite si certains des attributs sont gratuits, nous sommes donc sûrs que cet utilisateur sera unique.
Structure des validateurs:
Dans les deux, nous avons la même interface: (par exemple, interface pour les attributs)
package main.user.validator.attributesvalidators;
import main.entity.User;
public interface IUserAttributesValidator {
String validate(User user);
}
Ensuite, nous avons quelque chose qui est appelé (encore une fois pour attrubutes) UserAttributesValidator C'est la classe qui contient tous les autres validateurs et à l'intérieur de son constructeur, nous créons une liste de tous les validateurs afin que nous puissions tout parcourir en un seul flux.
package main.user.validator.attributesvalidators;
import main.entity.User;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class UserAttributesValidator {
final private List<IUserAttributesValidator> validators;
public UserAttributesValidator() {
validators = new ArrayList<>();
validators.add(new UserEmailValidator());
validators.add(new UserFirstNameValidator());
validators.add(new UserLastNameValidator());
validators.add(new UserPasswordValidator());
validators.add(new UserPhoneValidator());
validators.add(new UsernameValidator());
}
public List<String> validate(User user) {
return validators.stream()
.map(e -> e.validate(user))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
Nous obtenons une liste en sortie et c'est très bien. La même chose est faite pour AvailabilityValidator
Un sur le validateur par exemple:
package main.user.validator.attributesvalidators;
import main.entity.User;
public class UsernameValidator implements IUserAttributesValidator {
public static final int NAME_MAXIMUM_LENGTH = 30;
public static final int NAME_MINIMUM_LENGTH = 3;
public static final String NAME_ILLEGAL_CHARACTER_REGEX = "[A-Za-z0-9]+";
@Override
public String validate(User user) {
String attribute = user.getUsername();
if (attribute.length() > NAME_MAXIMUM_LENGTH) {
return "username is too long";
} else if (attribute.length() < NAME_MINIMUM_LENGTH) {
return "username is too short";
} else if (!attribute.matches(NAME_ILLEGAL_CHARACTER_REGEX)) {
return "username contains illegal character";
}
return null;
}
}
Maintenant, ma préoccupation est ... Je pense que c'est une mauvaise conception. Je veux dire, je crée l'interface IUserAttributesValidator, puis je crée une classe qui contient tous les autres validateurs qui ont également une méthode qui porte le même nom que la méthode dans l'interface. Je me demande si je peux fusionner ces deux en un seul? C'est possible? Existe-t-il une possibilité d'améliorer ce code? Je pense également que les deux (vérificateur de disponibilité et d'attributs) ont la même interface avec la même méthode mais des arguments différents, mais je ne sais pas s'il est également possible d'avoir une seule interface pour les deux.
Réponses
Comme je l'ai dit dans mon commentaire ci-dessous votre question, java couvre déjà votre besoin de définir des validateurs personnalisés pour vos données avant de les enregistrer dans la base de données. Le package couplé au springframework est le javax.validationpackage, donc en prenant par exemple votre code de validation personnalisé:
public class UsernameValidator implements IUserAttributesValidator {
public static final int NAME_MAXIMUM_LENGTH = 30;
public static final int NAME_MINIMUM_LENGTH = 3;
public static final String NAME_ILLEGAL_CHARACTER_REGEX = "[A-Za-z0-9]+";
@Override
public String validate(User user) {
String attribute = user.getUsername();
if (attribute.length() > NAME_MAXIMUM_LENGTH) {
return "username is too long";
} else if (attribute.length() < NAME_MINIMUM_LENGTH) {
return "username is too short";
} else if (!attribute.matches(NAME_ILLEGAL_CHARACTER_REGEX)) {
return "username contains illegal character";
}
return null;
}
}
Cela peut être remplacé en utilisant le mécanisme d'annotations directement à l'intérieur de votre Userclasse de cette manière:
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
public class User {
@Size(min=3, max=30, message="username length should be between 3 and 30 chars")
//it seems me username should contain just these chars
@Pattern(regexp="[A-Za-z0-9]+", message="username contains illegal characters")
private String username;
}
Le même mécanisme peut être appliqué aux autres champs de votre Userclasse lorsque vous en avez besoin de la même manière.
Ignorer la possibilité de l'implémenter avec les outils fournis par Spring Framework ... ce que vous avez là est assez proche d'être un modèle de conception composite , c'est donc une conception bien connue et acceptée. Ce que vous devez changer pour y parvenir, c'est que les validateurs individuels et le validateur composite implémentent exactement la même interface. Il est logique que le IUserAttributesValidatorretour a List<String>ou, Collection<String>comme dans votre exemple, UsernameValidatorpuisse trouver plus d'une violation dans l'entrée et il serait pratique de renvoyer à la fois les violations de longueur et de jeu de caractères à partir du même appel (tout en vérifiant la longueur et le jeu de caractères dans le même validateur sent un peu comme une violation de responsabilité unique).
En outre, au lieu de renvoyer une liste, vous pouvez passer une liste en tant que paramètre afin que chaque validateur puisse ajouter ses erreurs à une liste existante au lieu de créer une nouvelle liste jetable pour chaque erreur.
iterface Validator<T> {
void validate(T target, List<String> errors);
}