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

от автора

Проверка данных в приложении введённых пользователем или полученных другим путём в классическом понимании подразумевает использование всего лишь двух выражений в коде: 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.

Вариант метода аутентификации пользователя и возврат строки из метода, которая может содержать в себе как данные пользователя(будь то Id пользователя, XML или JSON) так и данные об ошибках и парситься на стороне клиента:

public String authUser(String username, String password, String email, Locale locale) { 	var validate = new LocalizedValidation.Default(); 	var errors = validate.apply("aaa", "qwe", "aaa@mail.ru", locale); 	if(!errors.isEmpty()) { 	return LocalizedValidation.FAIL+errors.stream().collect(Collectors.joining("<br/>")); 	}else { 	    try { 		var user = new User.Default(0L, username.trim(), email.trim(), new Encrypt().apply(password.trim()), "").create(dataSource); 		return LocalizedValidation.OK+user.toString();// or LocalizedValidation.Ok+user.id(); //or LocalizedValidation.Ok+user.toJson(); 		} catch (RuntimeException e) { 			return FAIL+e.getMessage(); 		} 	} } 

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

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

  1. Антипаттерны тестирования ПО
  2. Концепции автоматического тестирования

ссылка на оригинал статьи https://habr.com/ru/post/487838/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *