Рефакторинг системы валидатора
У меня есть система проверки, которая проверяет входные данные перед сохранением в БД. Допустим, я хочу создать нового пользователя. Мы находимся в классе обслуживания:
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);
}
}
У нас есть два валидатора - первый проверяет, в порядке ли атрибуты пользователя, а затем свободны ли некоторые из атрибутов, поэтому мы уверены, что этот пользователь будет уникальным.
Состав валидаторов:
В обоих случаях у нас одинаковый интерфейс: (например, интерфейс для атрибутов)
package main.user.validator.attributesvalidators;
import main.entity.User;
public interface IUserAttributesValidator {
String validate(User user);
}
Затем у нас есть что-то, что называется (снова для атрибутов) UserAttributesValidator. Это класс, который содержит все остальные валидаторы, и внутри его конструктора мы создаем список всех валидаторов, чтобы мы могли перебрать все в одном потоке.
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());
}
}
На выходе мы получаем список, и все в порядке. То же самое сделано для AvailabilityValidator
Один на валидаторе, например:
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;
}
}
Теперь меня беспокоит .... Я считаю, что это плохой дизайн. Я имею в виду, что я создаю интерфейс IUserAttributesValidator, затем создаю класс, содержащий все остальные валидаторы, у которых также есть метод, имя которого совпадает с именем метода в интерфейсе. Интересно, смогу ли я объединить эти два в один? Является ли это возможным? Есть ли возможность улучшить этот код? Другая моя мысль заключается в том, что оба (средство проверки доступности и атрибутов) имеют один и тот же интерфейс с одним и тем же методом, но с другим аргументом, но я не знаю, возможно ли иметь только один интерфейс для обоих.
Ответы
Как я сказал в своем комментарии под вашим вопросом, java уже покрывает вашу потребность в определении настраиваемых валидаторов для ваших данных перед их сохранением в базе данных. Пакет в сочетании с springфреймворком - это javax.validationпакет, поэтому, например, ваш собственный код валидатора:
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;
}
}
Это можно заменить с помощью механизма аннотаций непосредственно внутри вашего Userкласса следующим образом:
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;
}
Тот же механизм может быть применен к другим полям вашего Userкласса, когда вам нужно таким же образом.
Игнорирование возможности реализации этого с помощью инструментов, предоставляемых фреймворком Spring ... то, что у вас есть, довольно близко к составному шаблону проектирования, поэтому это хорошо известный и принятый дизайн. Для этого вам нужно изменить, чтобы и отдельные валидаторы, и составной валидатор реализовывали один и тот же интерфейс. Имеет смысл иметь IUserAttributesValidatorвозврат a List<String>или, Collection<String>как в вашем примере, во UsernameValidatorвходных данных может быть обнаружено более одного нарушения, и было бы удобно возвращать нарушения как длины, так и набора символов из одного и того же вызова (хотя проверка длины и набора символов в одном и том же валидатор действительно пахнет нарушением единой ответственности).
Кроме того, вместо возврата списка вы можете передать список в качестве параметра, чтобы каждый валидатор мог добавлять свои ошибки в существующий список вместо создания нового одноразового списка при каждой ошибке.
iterface Validator<T> {
void validate(T target, List<String> errors);
}