System walidacji refaktoryzacji

Aug 28 2020

Mam system walidacji, który sprawdza poprawność danych wejściowych przed zapisaniem do DB. Powiedzmy, że chcę utworzyć nowego użytkownika. Jesteśmy w klasie usług:

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);
    }
}

Mamy dwa walidatory - pierwszy, który sprawdzi, czy atrybuty użytkownika są w porządku, a następnie, czy niektóre atrybuty są wolne, więc mamy pewność, że ten użytkownik będzie unikalny.

Struktura walidatorów:

W obu mamy ten sam interfejs: (na przykład interfejs dla atrybutów)

package main.user.validator.attributesvalidators;

import main.entity.User;

public interface IUserAttributesValidator {

    String validate(User user);
}

Następnie otrzymaliśmy coś, co nazywa się (ponownie dla atrubutów) UserAttributesValidator Jest to klasa, która zawiera wszystkie inne walidatory, a wewnątrz jej konstruktora tworzymy listę wszystkich walidatorów, dzięki czemu możemy przechodzić przez wszystkie w jednym strumieniu.

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());
    }
}

Otrzymujemy listę jako wyjście i jest w porządku. To samo dotyczy AvailabilityValidator

Na przykład jeden na walidatorze:

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;
    }
}

Teraz martwię się… Myślę, że to zły projekt. Chodzi mi o to, że tworzę interfejs IUserAttributesValidator, następnie tworzę klasę, która zawiera wszystkie inne walidatory, które mają również metodę, która ma taką samą nazwę jak metoda w interfejsie. Zastanawiam się, czy mogę połączyć te dwa w jedno? Czy to możliwe? Czy jest jakaś możliwość ulepszenia tego kodu? Inną moją myślą jest to, że oba (sprawdzanie dostępności i atrybutów) mają ten sam interfejs z tą samą metodą, ale różnymi argumentami, ale nie wiem, czy można mieć tylko jeden interfejs dla obu.

Odpowiedzi

2 dariosicily Aug 28 2020 at 14:52

Jak powiedziałem w komentarzu pod pytaniem, java obejmuje już potrzebę zdefiniowania niestandardowych walidatorów dla danych przed zapisaniem ich w bazie danych. Pakiet w połączeniu z springframeworkiem jest javax.validationpakietem, więc na przykład twój niestandardowy kod walidatora:

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;
    }
}

Można to zastąpić za pomocą mechanizmu adnotacji bezpośrednio w Userklasie w następujący sposób:

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;
}

Ten sam mechanizm można zastosować do innych pól Twojej Userklasy, gdy potrzebujesz w ten sam sposób.

1 TorbenPutkonen Aug 28 2020 at 15:07

Pomijając możliwość zaimplementowania tego za pomocą narzędzi dostarczonych przez Spring Framework ... to, co masz, jest bardzo bliskie bycia wzorcem projektowym kompozytu, więc jest to projekt dobrze znany i akceptowany. Aby to osiągnąć, trzeba by zmienić, aby zarówno poszczególne walidatory, jak i walidator złożony, implementowały dokładnie ten sam interfejs. Sensowne jest IUserAttributesValidatorzwrócenie a List<String>lub, Collection<String>tak jak w naszym przykładzie, UsernameValidatormoże to znaleźć więcej niż jedno naruszenie w danych wejściowych i byłoby wygodnie zwrócić zarówno naruszenia długości, jak i zestawu znaków z tego samego wywołania (chociaż sprawdzanie długości i zestawu znaków w tym samym walidator pachnie trochę jak naruszenie pojedynczej odpowiedzialności).

Ponadto, zamiast zwracać listę, możesz przekazać listę jako parametr, tak aby każdy walidator mógł dołączyć swoje błędy do istniejącej listy, zamiast tworzyć nową listę jednorazowego użytku dla każdego błędu.

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