Refactoring-Validatorsystem
Ich habe ein Validierungssystem, das Eingabedaten validiert, bevor sie in der Datenbank gespeichert werden. Angenommen, ich möchte einen neuen Benutzer erstellen. Wir sind in der Serviceklasse:
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);
}
}
Wir haben zwei Validatoren - zuerst wird geprüft, ob die Attribute des Benutzers in Ordnung sind, und dann, ob einige der Attribute frei sind, sodass wir sicher sind, dass dieser Benutzer eindeutig ist.
Validatorstruktur:
In beiden haben wir die gleiche Schnittstelle: (zum Beispiel Schnittstelle für Attribute)
package main.user.validator.attributesvalidators;
import main.entity.User;
public interface IUserAttributesValidator {
String validate(User user);
}
Dann haben wir etwas, das (wieder für attrubutes) UserAttributesValidator heißt. Es ist die Klasse, die alle anderen Validatoren enthält, und innerhalb ihres Konstruktors erstellen wir eine Liste aller Validatoren, damit wir alle in einem Stream durchlaufen können.
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());
}
}
Wir bekommen eine Liste als Ausgabe und es ist in Ordnung. Gleiches gilt für AvailabilityValidator
Eine auf Validator zum Beispiel:
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;
}
}
Nun, meine Sorge ist ... Ich denke, dass dies schlechtes Design ist. Ich meine, ich erstelle die Schnittstelle IUserAttributesValidator, dann erstelle ich eine Klasse, die alle anderen Validatoren enthält, die auch eine Methode hat, deren Name mit der Methode in der Schnittstelle identisch ist. Ich frage mich, ob ich diese beiden zu einem zusammenführen kann. Ist es möglich? Gibt es eine Möglichkeit, diesen Code zu verbessern? Ein anderer Gedanke ist, dass beide (Verfügbarkeits- und Attributprüfung) dieselbe Schnittstelle mit derselben Methode, aber unterschiedlichen Argumenten haben, aber ich weiß nicht, ob es auch möglich ist, nur eine Schnittstelle für beide zu haben.
Antworten
Wie ich in meinem Kommentar unter Ihrer Frage sagte, deckt Java bereits Ihre Notwendigkeit ab, benutzerdefinierte Validatoren für Ihre Daten zu definieren, bevor Sie diese in der Datenbank speichern. Das mit dem springFramework gekoppelte Paket ist das javax.validationPaket. Nehmen Sie zum Beispiel Ihren benutzerdefinierten Validierungscode:
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;
}
}
Dies kann mithilfe des Mechanismus für Anmerkungen direkt in Ihrer UserKlasse folgendermaßen ersetzt werden:
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;
}
Der gleiche Mechanismus kann auf die anderen Felder Ihrer UserKlasse angewendet werden, wenn Sie dies auf die gleiche Weise benötigen.
Wenn Sie die Möglichkeit ignorieren, dies mit den von Spring Framework bereitgestellten Tools zu implementieren, ist das, was Sie dort haben, fast ein zusammengesetztes Entwurfsmuster, sodass es ein bekanntes und akzeptiertes Design ist. Um dies zu erreichen, müssten Sie sowohl die einzelnen Validatoren als auch den zusammengesetzten Validator genau dieselbe Schnittstelle implementieren. Es ist sinnvoll, die IUserAttributesValidatorRückgabe a zu haben List<String>oder Collection<String>wie in Ihrem Beispiel UsernameValidatormehr als eine Verletzung in der Eingabe zu finden, und es wäre zweckmäßig, sowohl Längen- als auch Zeichensatzverletzungen von demselben Aufruf zurückzugeben (obwohl Länge und Zeichensatz in derselben überprüft werden Der Validator riecht ein bisschen nach einer Verletzung der Einzelverantwortung.
Anstatt eine Liste zurückzugeben, können Sie auch eine Liste als Parameter übergeben, damit jeder Validator seine Fehler an eine vorhandene Liste anhängen kann, anstatt bei jedem Fehler eine neue Einwegliste zu erstellen.
iterface Validator<T> {
void validate(T target, List<String> errors);
}