Pull to refresh

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

Reading time5 min
Views8.3K
Проверка данных в приложении введённых пользователем или полученных другим путём в классическом понимании подразумевает использование всего лишь двух выражений в коде: 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. Концепции автоматического тестирования
Tags:
Hubs:
0
Comments11

Articles