valiktorを使ってVO値のバリデーションを行う

今回は自身で開発・運営している「Hello Books」で利用している技術について紹介したいと思います。  
  
#### [Hello Books](https://hellobooks.org)について
https://hellobooks.org

* エンジニア向け技術書レビューサービス
* 購入前の判断にエンジニアのレビューを役立てることができる
* アカウントを作成することで自身もレビューを投稿したり、技術書をブックマークしたりできる
* 詳細な条件による技術書の検索が可能
![スクリーンショット 2021-05-27 10.20.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/521599/52eb5728-a133-3759-011d-95571d1f006d.png)

#### valiktorとは
> Valiktor is a type-safe, powerful and extensible fluent DSL to validate objects in Kotlin.  
https://github.com/valiktor/valiktor

オブジェクトのバリデーションをサポートしてくれるKotlinライブラリで、シンプルな記述で様々なバリデーションを実装できるのが魅力です。[Hello Books](https://hellobooks.org)ではDDD（ドメイン駆動設計）に基づいて設計・実装を行っていますが、Entityを構成するValue Objectを生成する際にその値の妥当性を担保するためにvaliktorを利用しています。


#### シンプルなBook Entityでの利用例を見てみる
一例として、下記の様な非常にシンプルなEntity "Book"を想定します。

|  Book  | 
| :---: |
| id: BookId | 
| author: BookAuthor | 
| title: BookTitle | 
| price: BookPrice | 
| publishedAt: BookPubishedAt | 

Book Entityを構成する5つのValue Objectをそれぞれ定義しています。DDDでは、Entityを生成する際にはその妥当性を担保する必要があります。例えばpriceが0円以下の場合などは現実の書籍を想定するとあり得ないため、Value Object生成時にその様なケースはValidationで弾く実装を行う必要があります。
様々なアプローチがあると思いますが、valiktorを利用するとすごく簡単に実装することができます。

#### BookPriceでのvaliktor実装例
```kotlin
import org.valiktor.functions.isGreaterThan
import org.valiktor.validate

data class BookPrice(var value: Int) {

    init {
        validation()
    }

    /**
     * validation rule
     */
    fun validation() {
        validate(this) {
            validate(BookPrice::value)
                .isGreaterThan(0)
                .isLessThanOrEqualTo(10000)
        }
    }
}
```
[Hello Books](https://hellobooks.org)では抽象クラスを利用してバリデーションなどの実装を定義していますが、単純化のためクラス内で完結する実装を例として扱います。
kotlinでは`init`ブロックでプライマリコンストラクタが呼び出されたときに実行される処理を定義することができます。`validation`メソッドでvaliktorを用いたバリデーションを実装しています。メソッドチェインの形で複数条件を設定することができ、今回のケースでは"書籍の価格は0円以上10,000円以下でなければいけない"としてバリデーションを設定しています。

#### ユニットテストでバリデーションの実装を確認
```kotlin
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.valiktor.ConstraintViolationException

class BookPriceTest {
    companion object {
        /**
         * data provider methods for "testValidation"
         */
        @JvmStatic
        fun dataProvider() = listOf(
            // normal cases
            Arguments.of(1, true),
            Arguments.of(100000, true),
            // exceptional cases
            Arguments.of(0, false),
        )
    }

    /**
     * validation test checking if provided parameters are valid for the VO
     */
    @ParameterizedTest
    @MethodSource("dataProvider")
    fun testValidation(value: Int, isValid: Boolean) {
        when (isValid) {
            true -> assertDoesNotThrow { BookPrice(value) }
            false -> assertThrows<ConstraintViolationException> { BookPrice(value) }
        }
    }
}
```

junitのParameterizedTestを利用することで`dataProvider`で定義している各値で`BookPrice`インスタンスを生成した場合にバリデーション例外が発生するかを確認しています。データドリブンテストについては[こちらの記事](https://qiita.com/Takuyaaaa/items/0557fd6af1a5699ca63c)詳しく紹介しています。
実際にテストを走らせると、通過したことを無事確認できました！


今回は数字の上下限に対するバリデーション実装のみ扱いましたが、他にも`hasSize`で最小/最大長を設定したり、`.isEmail()`でメールアドレス形式を保証したりなど様々な機能を利用することができます。[公式のドキュメント](https://github.com/valiktor/valiktor)で分かりやすくまとめられているので、興味のある方は見てみてください。

##### 最後に
最後まで読んでくださり、ありがとうございました。よろしければ[Hello Books](https://hellobooks.org)にも是非遊びに来てください！