Валидация данных: другой подход

    Проверка данных в приложении введённых пользователем или полученных другим путём в классическом понимании подразумевает использование всего лишь двух выражений в коде: TRUE и FALSE. В другом варианте используют исключения которые явно не предназначены для этого. Есть ли вариант получше?

    Проверкой занимаются так называемые Валидаторы(которые являются лишь частью всего процесса проверки данных). В статье Серверная валидация пользовательских данных приводится интересный вариант реализации валидатора, но есть несколько нюансов в виде локализации сообщений и самого формата ошибок.

    Рассмотрим сначала формат ошибок.

    Предлагаемый подход состоит в том, чтобы метод валидатора, проверяющий данные, возвращал коллекцию(массив, список и т.д.) строк вместо булевых значений или бросания исключений. Такой формат будет более гибким и информативным.

    Приведу пример на Java:

    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public interface ValidateUser {
    	String OK = "OK:";
    	String FAIL= "FAIL:";
    	Collection<String> apply(String username, String password, String email);
    	default Collection<String> passwordValidate(String password){
    		var result = new ArrayList<String>(1);
    		int size = password.trim().length();
    		if(size < 3 || size > 20) result.add("Password error:too short value or too long name. Password must be greater or 3 characters and smaller then 20 simbols.");
    		return result;
    	}
    	default Collection<String> usernameValidate(String name){
    		var result = new ArrayList<String>(1);
    		int size = name.trim().length();
    		if(size < 3 || size > 30) result.add("Username error:too short or too long name. Name must be greater or 3 characters and smaller then 30 simbols.");
    		return result;
    	}
    	default Collection<String> emailValidate(String email){
    		var result = new ArrayList<String>(1);
    		String regex = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
    		Pattern pattern = Pattern.compile(regex);
    		Matcher matcher = pattern.matcher(email);
    		if (!matcher.find()) result.add("Email error:" + email + " is not valid");
    		return result;
    	}
    	class Default implements ValidateUser{
    
    		@Override
    		public Collection<String> apply(String username, String password,
    				String email) {
    			var errors = passwordValidate(password.trim());
    			errors.addAll(usernameValidate(username.trim()));
    			errors.addAll(emailValidate(email.trim()));
    			return errors;
    		}
    		
    	}
    }
    

    Как здесь видно все методы возвращают коллекцию строк.

    Пример Unit теста:

            @Test
    	void validateUserTest() {
    		
    		var validate = new ValidateUser2.Default();
    		var result = validate.apply("aaa", "qwe", "aaa@mail.ru");
    		assertTrue(result.isEmpty());
    		result = validate.apply("aaa", "qwe", "");
    		assertFalse(result.isEmpty());
    		assertEquals(1, result.size());
    		
    		result = validate.apply("aa", "qwe", "aaa@mail.ru");
    		assertFalse(result.isEmpty());
    		assertEquals(1, result.size());
    		
    		result = validate.apply("aaa", "qwe", "@mail.qweqwe");
    		assertFalse(result.isEmpty());
    		assertEquals(1, result.size());
    		
    		result = validate.apply("aa", "qw", "");
    		assertFalse(result.isEmpty());
    		assertEquals(3, result.size());
    	}
    

    А теперь рассмотрим локализацию сообщений ошибок.

    Пример снова на Java:

    public interface LocalizedValidation {
    	String OK = "OK:";
    	String FAIL= "FAIL:";
    	Collection<String> apply(String username, String password, String email, Locale locale);
    	default Collection<String> passwordValidate(String password, ResourceBundle bundle){
    		var result = new ArrayList<String>(1);
    		int size = password.trim().length();
    		if(size < 3 || size > 20) result.add(bundle.getString("password"));
    		return result;
    	}
    	default Collection<String> usernameValidate(String name, ResourceBundle bundle){
    		var result = new ArrayList<String>(1);
    		int size = name.trim().length();
    		if(size < 3 || size > 30) result.add(bundle.getString("username"));
    		return result;
    	}
    	default Collection<String> emailValidate(String email, ResourceBundle bundle){
    		var result = new ArrayList<String>(1);
    		String regex = "^[\\w!#$%&'*+/=?`{|}~^-]+(?:\\.[\\w!#$%&'*+/=?`{|}~^-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$";
    		Pattern pattern = Pattern.compile(regex);
    		Matcher matcher = pattern.matcher(email);
    		if (!matcher.find()) result.add(bundle.getString("username")+email);
    		return result;
    	}
    	class Default implements LocalizedValidation{
    
    		@Override
    		public Collection<String> apply(String username, String password,
    				String email, Locale locale) {
    			ResourceBundle bundle = ResourceBundle.getBundle("errors", locale);
    			var errors = passwordValidate(password.trim(), bundle);
    			errors.addAll(usernameValidate(username.trim(), bundle));
    			errors.addAll(emailValidate(email.trim(), bundle));
    			return errors;
    		}
    	}
    }
    

    
    	@Test
    	void localizedUserTest() {
    		var validate = new LocalizedValidation.Default();
    		var result = validate.apply("aaa", "qwe", "aaa@mail.ru", Locale.ENGLISH);
    		assertTrue(result.isEmpty());
    		result = validate.apply("aaa", "qwe", "", Locale.ENGLISH);
    		assertFalse(result.isEmpty());
    		assertEquals(1, result.size());
    		System.out.println(result.iterator().next());
    		
    		result = validate.apply("aaa", "qwe", "", new Locale("ru"));
    		assertFalse(result.isEmpty());
    		assertEquals(1, result.size());
    		System.out.println(result.iterator().next());
    	}
    

    Файлы локализаций лежат в src/main/resources.

    errors_ru.properties:

    mail=Email ошибка: не верный Email:
    username=Ошибка имени пользователя: слишком короткое или длинное имя.
    password=Ошибка в длине пароля: пароль слишком длинный.

    errors.properties:

    mail=Email error: is not valid:
    username=Username error:too short or too long name. Name must be greater or 3 characters and smaller then 30 simbols.
    password=Password error:too short value or too long name. Password must be greater or 3 characters and smaller then 20 simbols.

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

    P.S.

    Вариант метода аутентификации пользователя, который возвращает реализацию интерфейса java.util.Map.Entry<K, V>, и может содержать в себе как объект пользователя так и данные об ошибках в виде строки(такой подход также может использоваться для избежания возврата Null из метода когда ожидается объект пользователя):

    @Override
    public Entry<String, User> auth(String username, String password, String email) {
    	var errors = passwordValidation.apply(password.trim());
    	errors.addAll(userNameValidation.apply(username.trim()));
    	errors.addAll(emailValidation.apply(email.trim()));
    	if(!errors.isEmpty()) {
    		return REntry.ofI(null, errors.stream().collect(Collectors.joining(";")));
    	}else {
    		try {
    			var passwordEncrypted = new Encrypt().apply(password.trim());
    			var user = new User.Default(0L, username.trim(), email.trim(), passwordEncrypted, "").create(dataSource);
    			return REntry.ofI(user, "user is null!");
    		} catch (RuntimeException e) {
    			return REntry.ofI(null, e.getMessage());
    		}
    	}
    }
    

    Есть, конечно, замечательная статья на хабре про валидацию в Java, но подход который там предлагается может использоваться далеко не во всех случаях и основан на аннотациях и исключениях.

    В окончание статьи пару полезных ссылок про тестирование:

    1. Антипаттерны тестирования ПО
    2. Концепции автоматического тестирования
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 11

      +3
      Проверка данных в приложении введённых пользователем или полученных другим путём в классическом понимании подразумевает использование всего лишь двух выражений в коде: TRUE и FALSE.

      Что за "классическое понимание", и где вы его взяли? В .net IValidatableObject.Validate возвращает коллекцию ValidationResult, а ModelState (в ASP.net MVC) содержит коллекцию ModelError.

        +1
        Так и в Java есть Jakarta Bean Validation, в котором Validator.validate() возвращает Set<ConstraintViolation>.
        Автор просто решил изобрести велосипед видимо.
          –2
          Не всегда эту реализацию возможно использовать в проекте. Например: в JavaSE или android. Плюс к этому там используются исключения при валидации.
            +1
            Не всегда эту реализацию возможно использовать в проекте. Например: в JavaSE или android.

            В Java SE её можно использовать:
            This document is the specification of the Jakarta Bean Validation in Jakarta EE and Java SE.
            Про Android я мало что знаю, но там, скорее всего, тоже есть что-то готовое.

            Плюс к этому там используются исключения при валидации.

            Если вы про Exception model, то это исключения, выбрасываемые в случае неправильной конфигурации валидатора.
            Сама валидация возвращает Set<ConstraintViolation>.
              +1
              Нет, я имел ввиду что при проверках(под капотом этой реализации) бросаются исключения, которые потом оборачиваются в Set<ConstraintViolation>. ValidationException — один из основных. Вот здесь больше описано про исключения.
              А с JavaSE я был не прав. Но ведь такой подход применим и для других языков, а не только там где есть такие вещи как JSR-303, JSR-349, JSR-380.
                0
                Такой подход уже сто лет как используется везде и повсеместно, особенно в вебе.
                Единственные исключения это сайты, где разработчики особо не заморачиваются.

                А ещё вы можете возвращать код ошибки, а описание ошибок хранить на клиенте.
                Или же можно возвращать несколько кодов ошибок, а ещё лучше использовать побитовую маску и объединить все ошибки в одно значение.

                Почему то ещё сразу вспомнил WSDL.
                  0
                  Я думаю что данная статья и поможет использовать именно такой подход для валидации данных. А с Вашей стороны было бы хорошо подсказать: какие валидаторы лучше всего использовать в вебе. Вот Вы каким пользуетесь?
                    0
                    У меня есть свой, давно написанный шаблонный класс, который я дописываю под задачу. Максимально сурово контролируя все данные, с жёсткой типизацией, длинной, проверкой по спискам и тп.

                    Ну или использую решения из фрейморков, если он используется на проекте и может справиться с задачей. Опять же дополнительно проводя проверки где это необходимо.

                    А по поводу информирования пользователей, так же всё зависит от задачи и объёмов работ.
                    Если использую свой шаблонный класс, то он как раз возвращает код ошибок, так же есть шаблонный класс js который содержит практически тоже самое, так как не за чем гонять туда-сюда заведомо неверную информацию.

                    Бывает делаю и в тупую, где вся форма выдаёт ошибку или же не выдаёт. Но это опять же от проекта зависит и задач.

                    В данном случаи считаю нету типовых решений, есть только правила, до которых всем надо постепенно дойти, самое первое, это максимально возможно жёстко контролировать все входящие данные. А остальное придёт с опытом, как и собственные наработки.
                  0
                  Нет, я имел ввиду что при проверках(под капотом этой реализации) бросаются исключения, которые потом оборачиваются в Set ConstraintViolation. ValidationException — один из основных. Вот здесь больше описано про исключения.


                  Я в документ, что вы привели, не вчитывался, но похоже, что там как раз наоборот.
                  Результат валидации оборачивается в исключение, потому что этого требует платформа CUBA:
                          Set<ConstraintViolation<Product>> product_violations = validator.validate(product);
                          if (product_violations.size() > 0) {
                              StringBuilder strBuilder = new StringBuilder();
                              product_violations.stream().forEach(violation -> strBuilder.append(violation.getMessage()).append("; "));
                              throw new CustomValidationException(strBuilder.toString());
                          }
                  

                  Если вы имеете ввиду примеры из Setting validator programmatically:
                          quantityField.addValidator(
                                  new Field.Validator() {
                                      @Override
                                      public void validate(Object value) throws ValidationException {
                                          if (value != null && value instanceof BigDecimal
                                                  && ((BigDecimal)value).compareTo(new BigDecimal(1000)) > 0) {
                                              throw new ValidationException(getMessage("quantityIsTooBig"));
                                          }
                                      }
                                  }
                          );

                  То там тоже исключения кидаются потому что этого хочет CUBA:
                      @Deprecated
                      interface Validator<T> extends Consumer<T> {
                          /**
                           * @param value field value to validate
                           * @throws ValidationException this exception must be thrown by the validator if the value is not valid
                           */
                          void validate(@Nullable Object value) throws ValidationException;
                  
                          @Override
                          default void accept(T t) {
                              validate(t);
                          }
                      }
                  

                  Но это уже личные проблемы платформы CUBA.
                  Ни спецификация, что я привёл, ни её реализации не требуют выброса исключений для возврата результата валидации и не делают этого.
                  Хотя и не запрещают этого, если фреймворк требует.

                  А с JavaSE я был не прав. Но ведь такой подход применим и для других языков, а не только там где есть такие вещи как JSR-303, JSR-349, JSR-380.

                  Можете привести пример языка, в котором нет готовой библиотеки для валидации?
                    0
                    Я же имел ввиду спецификации, а не библиотеки. Библиотек много(платформа CUBA, реализация от Hibernate и т.д.), но вот стандарта единого(такого как JSR-303, JSR-349, JSR-380) нет. К тому же многие пишут свои реализации валидаторов, а не используют готовые библиотеки.
                      0
                      но вот стандарта единого

                      Между языками? А зачем?

        Only users with full accounts can post comments. Log in, please.