«1. Обзор

Проверка — часто встречающаяся задача в Java-приложениях, поэтому в разработку библиотек проверки было вложено много усилий.

Vavr (ранее известный как Javaslang) предоставляет полноценный API проверки. Это позволяет нам проверять данные простым способом, используя объектно-функциональный стиль программирования. Если вы хотите взглянуть на то, что эта библиотека предлагает из коробки, не стесняйтесь проверить эту статью.

В этом руководстве мы подробно рассмотрим API проверки библиотеки и узнаем, как использовать его наиболее важные методы.

2. Интерфейс проверки

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

Аппликативный функтор библиотеки основан на реализации ее интерфейса Validation. Этот интерфейс предоставляет методы для накопления ошибок проверки и проверенных данных, что позволяет обрабатывать их как пакет.

3. Проверка пользовательского ввода

Проверка пользовательского ввода (например, данных, собранных из веб-уровня) выполняется гладко с помощью API проверки, поскольку она сводится к созданию пользовательского класса проверки, который проверяет данные, накапливая возникающие ошибки, если любой.

Давайте проверим имя пользователя и адрес электронной почты, которые были отправлены через форму входа. Во-первых, нам нужно включить артефакт Vavr Maven в файл pom.xml:

<dependency>
    <groupId>io.vavr</groupId>
    <artifactId>vavr</artifactId>
    <version>0.9.0</version>
</dependency>

Затем давайте создадим класс предметной области, который моделирует пользовательские объекты:

public class User {
    private String name;
    private String email;
    
    // standard constructors, setters and getters, toString
}

Наконец, давайте определим наш пользовательский валидатор: ~~ ~

public class UserValidator {
    private static final String NAME_PATTERN = ...
    private static final String NAME_ERROR = ...
    private static final String EMAIL_PATTERN = ...
    private static final String EMAIL_ERROR = ...
	
    public Validation<Seq<String>, User> validateUser(
      String name, String email) {
        return Validation
          .combine(
            validateField(name, NAME_PATTERN, NAME_ERROR),
            validateField(email, EMAIL_PATTERN, EMAIL_ERROR))
          .ap(User::new);
    }
	
    private Validation<String, String> validateField
      (String field, String pattern, String error) {
 
        return CharSeq.of(field)
          .replaceAll(pattern, "")
          .transform(seq -> seq.isEmpty() 
            ? Validation.valid(field) 
            : Validation.invalid(error));		
    }
}

Класс UserValidator проверяет указанные имя и адрес электронной почты по отдельности с помощью метода validateField(). В этом случае этот метод выполняет типичное сопоставление шаблонов на основе регулярных выражений.

Суть этого примера в использовании методов valid() , invalid() и comb() .

4. Методы valid(), invalid() и comb()

Если предоставленное имя и адрес электронной почты соответствуют заданным регулярным выражениям, метод validateField() вызывает valid() . Этот метод возвращает экземпляр Validation.Valid. И наоборот, если значения недействительны, противоположный метод invalid() возвращает экземпляр Validation.Invalid.

Этот простой механизм, основанный на создании разных экземпляров Validation в зависимости от результатов проверки, должен дать нам хотя бы общее представление о том, как обрабатывать результаты (подробнее об этом в разделе 5).

Наиболее важным аспектом процесса проверки является метод Combine(). Внутри этот метод использует класс Validation.Builder, который позволяет комбинировать до 8 различных экземпляров Validation, которые могут быть вычислены разными методами:

static <E, T1, T2> Builder<E, T1, T2> combine(
  Validation<E, T1> validation1, Validation<E, T2> validation2) {
    Objects.requireNonNull(validation1, "validation1 is null");
    Objects.requireNonNull(validation2, "validation2 is null");
    return new Builder<>(validation1, validation2);
}

Самый простой класс Validation.Builder принимает два экземпляра Validation:

final class Builder<E, T1, T2> {

    private Validation<E, T1> v1;
    private Validation<E, T2> v2;

    // standard constructors

    public <R> Validation<Seq<E>, R> ap(Function2<T1, T2, R> f) {
        return v2.ap(v1.ap(Validation.valid(f.curried())));
    }

    public <T3> Builder3<E, T1, T2, T3> combine(
      Validation<E, T3> v3) {
        return new Builder3<>(v1, v2, v3);
    }
}

~~ ~ Validation.Builder вместе с методом ap(Function) возвращает один единственный результат с результатами проверки. Если все результаты допустимы, метод ap(Function) сопоставляет результаты с одним значением. Это значение сохраняется в экземпляре Valid с помощью функции, указанной в его сигнатуре.

В нашем примере, если указанные имя и адрес электронной почты допустимы, создается новый объект User. Конечно, можно сделать что-то совершенно другое с действительным результатом, то есть спрятать его в базу данных, отправить по электронной почте и так далее.

5. Обработка результатов проверки

Реализовать различные механизмы обработки результатов проверки достаточно просто. Но как мы проверяем данные в первую очередь? Для этого мы используем класс UserValidator:

UserValidator userValidator = new UserValidator(); 
Validation<Seq<String>, User> validation = userValidator
  .validateUser("John", "[email protected]");

После получения экземпляра Validation мы можем использовать гибкость API проверки и обрабатывать результаты несколькими способами.

Остановимся подробнее на наиболее часто встречающихся подходах.

5.1. Действительные и недействительные экземпляры

Этот подход на сегодняшний день является самым простым. Он состоит из проверки результатов проверки с экземплярами Valid и Invalid:

@Test
public void 
  givenInvalidUserParams_whenValidated_thenInvalidInstance() {
    assertThat(
      userValidator.validateUser(" ", "no-email"), 
      instanceOf(Invalid.class));
}
	
@Test
public void 
  givenValidUserParams_whenValidated_thenValidInstance() {
    assertThat(
      userValidator.validateUser("John", "[email protected]"), 
      instanceOf(Valid.class));
}

«

«Вместо того, чтобы проверять достоверность результатов с экземплярами Valid и Invalid, мы должны сделать еще один шаг и использовать методы isValid() и isInvalid().

5.2. API isValid() и isInvalid()


@Test
public void 
  givenInvalidUserParams_whenValidated_thenIsInvalidIsTrue() {
    assertTrue(userValidator
      .validateUser("John", "no-email")
      .isInvalid());
}

@Test
public void 
  givenValidUserParams_whenValidated_thenIsValidMethodIsTrue() {
    assertTrue(userValidator
      .validateUser("John", "[email protected]")
      .isValid());
}

Использование тандема isValid() / isInvalid() аналогично предыдущему подходу, с той разницей, что эти методы возвращают true или false в зависимости от результатов проверки:

@Test
public void 
  givenInValidUserParams_withGetErrorMethod_thenGetErrorMessages() {
    assertEquals(
      "Name contains invalid characters, Email must be a well-formed email address", 
      userValidator.validateUser("John", "no-email")
        .getError()
        .intersperse(", ")
        .fold("", String::concat));
 }

~ ~~ Экземпляр Invalid содержит все ошибки проверки. Их можно получить с помощью метода getError():

@Test
public void 
  givenValidUserParams_withGetMethod_thenGetUserInstance() {
    assertThat(userValidator.validateUser("John", "[email protected]")
      .get(), instanceOf(User.class));
 }

И наоборот, если результаты верны, экземпляр пользователя можно получить с помощью метода get():

Этот подход работает, как и ожидалось, но код по-прежнему выглядит довольно многословным и длинным. Мы можем дополнительно сжать его, используя метод toEither().

5.3. API toEither()

Метод toEither() создает экземпляры Left и Right интерфейса Both. Этот дополнительный интерфейс имеет несколько удобных методов, которые можно использовать для сокращения времени обработки результатов проверки.

@Test
public void 
  givenValidUserParams_withtoEitherMethod_thenRightInstance() {
    assertThat(userValidator.validateUser("John", "[email protected]")
      .toEither(), instanceOf(Right.class));
}

Если результаты действительны, результат сохраняется в экземпляре Right. В нашем примере это будет допустимый объект User. И наоборот, если результаты недействительны, ошибки сохраняются в экземпляре Left:

Код теперь выглядит гораздо более лаконичным и упорядоченным. Но мы еще не закончили. Интерфейс Validation предоставляет метод fold(), который применяет пользовательскую функцию, которая применяется к действительным результатам, а другую — к недействительным.

5.4. API fold()

@Test
public void 
  givenValidUserParams_withFoldMethod_thenEqualstoParamsLength() {
    assertEquals(2, (int) userValidator.validateUser(" ", " ")
      .fold(Seq::length, User::hashCode));
}

Давайте посмотрим, как использовать метод fold() для обработки результатов проверки:


Использование fold() сокращает обработку результатов проверки до одной строки.

Стоит подчеркнуть, что возвращаемые типы функций, передаваемые в качестве аргументов метода, должны быть одинаковыми. Более того, функции должны поддерживаться параметрами типа, определенными в классе проверки, т. е. Seq\u003cString\u003e и User.

6. Заключение

В этой статье мы подробно изучили API проверки Vavr и узнали, как использовать некоторые из его наиболее важных методов. Полный список см. в официальной документации API.

Управление проверкой Vavr представляет собой очень привлекательную альтернативу более традиционным реализациям проверки Java Beans, таким как Hibernate Validator.