Vavrの検証APIの紹介

1概要

検証はJavaアプリケーションで頻繁に発生するタスクであるため、検証ライブラリの開発に多大な努力が注がれています。

Vavr (以前はJavaslangと呼ばれていました)は、本格的なhttp://www.vavr.io/vavr-docs/#__validation[validation API]を提供します。

オブジェクト関数型プログラミングスタイルを使用することによって、データを簡単に検証できます。このライブラリが箱から出して何を提供しているのかを調べたい場合は、 この記事 をチェックしてください。

このチュートリアルでは、ライブラリの検証APIを詳しく調べ、その最も関連性の高いメソッドの使い方を学びます。

2. 検証 インターフェース

Vavrの検証インターフェースはhttps://en.wikipedia.org/wiki/Applicative__functor[applicative functor]として知られる関数型プログラミングの概念に基づいています。

実行連鎖中にこれらの関数の一部または全部が失敗した場合でも、結果を累積しながら一連の関数を実行します。

ライブラリのアプリケーションファンクタは、 Validation インタフェースの実装者に基づいて構築されています。このインタフェースは、検証エラーと検証済みデータを蓄積するためのメソッドを提供します。したがって、それらの両方をバッチとして処理することができます。

3ユーザー入力の検証

ユーザ入力(例えば、ウェブ層から収集されたデータ)を検証することは、検証APIを使用することがスムーズであり、結果として生じるエラーをもしあれば蓄積しながらデータを検証するカスタム検証クラスを作成することになる。

ログインフォームから送信されたユーザーの名前とメールアドレスを確認しましょう。まず、https://search.maven.org/classic/#search%7Cga%7C1%7Cvavr[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() 、および combine() メソッドの使用です。

4 valid()、 invalid() 、および combine() メソッド

指定された名前と電子メールが指定された正規表現と一致する場合、 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 クラスは、2つの検証インスタンスを取ります。

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 インスタンス

この方法は、これまでで最も簡単な方法です。検証結果を 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. isValid() および isInvalid() API

タンデム isValid() / isInvalid() を使用することは、検証結果に応じて、これらのメソッドが true または false を返すという点を除いて、前のアプローチと似ています。

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

Invalid インスタンスには、すべての検証エラーが含まれています。それらは getError() メソッドで取得することができます。

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

逆に、結果が有効であれば、 get()メソッドで User__インスタンスを取得できます。

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

このアプローチは予想通りに機能しますが、コードはまだかなり冗長で長く見えます。 toEither() メソッドを使用してさらに圧縮することができます。

5.3. toEither() API

toEither() メソッドは、 Either インターフェースの Left および Right インスタンスを構成します。この補完的なインタフェースには、検証結果の処理を短縮するために使用できるいくつかの便利なメソッドがあります。

結果が有効な場合、結果は Right インスタンスに格納されます。

この例では、これは有効な User オブジェクトになります。逆に、結果が無効な場合、エラーは Left インスタンスに格納されます。

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

コードはより簡潔で合理的になりました。しかし、まだ終わっていません。 Validation インターフェースは fold() メソッドを提供します。これは有効な結果に適用するカスタム関数と無効な結果に適用するカスタム関数を適用します。

5.4. fold() API

検証結果を処理するための fold() メソッドの使用方法を見てみましょう。

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

__fold()を使用すると、検証結果の処理が1行に短縮されます。

メソッドの引数として渡される関数の戻り値の型は同じでなければならないことを強調する価値があります。さらに、これらの関数は検証クラスで定義されている型パラメータ、つまり Seq <String> User によってサポートされている必要があります。

6. 結論

この記事では、Vavrの検証APIについて詳しく調べ、その最も関連性の高い方法のいくつかを使用する方法を学びました。完全なリストについては、http://www.javadoc.io/doc/io.vavr/vavr/0.9.0[公式ドキュメントAPI]を参照してください。

Vavrの検証コントロールは、http://hibernate.org/validator/[Hibernate Validator]のようなhttp://beanvalidation.org/[Java Beans Validation]のより伝統的な実装に対する非常に魅力的な代替手段を提供します。

いつものように、この記事に示されている例はすべてhttps://github.com/eugenp/tutorials/tree/master/vavr[GitHubで利用可能]です。