Рефакторинг системы валидатора

Aug 28 2020

У меня есть система проверки, которая проверяет входные данные перед сохранением в БД. Допустим, я хочу создать нового пользователя. Мы находимся в классе обслуживания:

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, затем создаю класс, содержащий все остальные валидаторы, у которых также есть метод, имя которого совпадает с именем метода в интерфейсе. Интересно, смогу ли я объединить эти два в один? Является ли это возможным? Есть ли возможность улучшить этот код? Другая моя мысль заключается в том, что оба (средство проверки доступности и атрибутов) имеют один и тот же интерфейс с одним и тем же методом, но с другим аргументом, но я не знаю, возможно ли иметь только один интерфейс для обоих.

Ответы

2 dariosicily Aug 28 2020 at 14:52

Как я сказал в своем комментарии под вашим вопросом, 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класса, когда вам нужно таким же образом.

1 TorbenPutkonen Aug 28 2020 at 15:07

Игнорирование возможности реализации этого с помощью инструментов, предоставляемых фреймворком Spring ... то, что у вас есть, довольно близко к составному шаблону проектирования, поэтому это хорошо известный и принятый дизайн. Для этого вам нужно изменить, чтобы и отдельные валидаторы, и составной валидатор реализовывали один и тот же интерфейс. Имеет смысл иметь IUserAttributesValidatorвозврат a List<String>или, Collection<String>как в вашем примере, во UsernameValidatorвходных данных может быть обнаружено более одного нарушения, и было бы удобно возвращать нарушения как длины, так и набора символов из одного и того же вызова (хотя проверка длины и набора символов в одном и том же валидатор действительно пахнет нарушением единой ответственности).

Кроме того, вместо возврата списка вы можете передать список в качестве параметра, чтобы каждый валидатор мог добавлять свои ошибки в существующий список вместо создания нового одноразового списка при каждой ошибке.

iterface Validator<T> {
    void validate(T target, List<String> errors);
}