複合Validationの実装を考えてみる

[Swift その2 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/swift2)の8日目の記事です。

## はじめに
クライアントサイドで行う簡易的なバリデーションチェックの実装を考えてみます。
ここで指すバリデーションチェックでは、最初に発生したエラーだけではなく入力した内容が間違っている際に全てのエラーを通知するものと定義。

愚直に実装すると

```swift
func validation(email: String, Password: Int) -> [String] {
    var acc: [String] = []
    if name.isEmpty { acc.append("名前が未入力です") }
    if age < 18 { acc.append("未成年です") }
    return acc
}

if validation(name: "", age: 0).isEmpty {
    // 正常処理
} else {
    // 異常処理
}
```

上記のように、一時格納(accumulator)用の配列を定義し、ここにエラーメッセージを格納する実装が考えられます。(validationメソッドの戻り値が空配列の場合に正常とみなす)

##### 問題点
- validationメソッド内に複数のバリデーションルールが集約され肥大化する恐れがある
- 単一責務の原則から外れ、また単体テストが書きづらい

このような問題点を抱えヤモヤしていた際に[Composite Validators - Hot Cocoa Touch](http://hotcocoatouch.com/2016/11/16/composite-validators/)の記事を読み、大変興味深く勉強になったので紹介させてください(著者から了承得ています)。

## ユースケース
アプリケーションに以下2つのフィールドを持つ登録フォームがあるとします。

[Email]

- 空白は許容しない
- メール形式に準拠している

[Password]

- 空白は許容しない
- 最低8文字以上の長さ
- 1つ以上の大文字が含まれている[^1]

### 準備
#### Validator Protocol
全てのvalidatorが実装するI/Fを作成します。
(ここでいうvalidatorは、ユーザー入力が有効かどうかを判断するオブジェクトと定義)

まず、validatorが返す結果型を作成します。
[Result](https://github.com/antitypical/Result)型を用いての実装も可能ですが、今回正常値は必要ないので専用の型を用意しました。

また、最初に発生したエラーだけではなく、全てのエラーを通知できるようError型を配列で保持するようにします。

```swift:ValidationResult
enum ValidationResult {
    case valid
    case invalid(errors: [Error])
}
```

各validatorは、与えられた入力値が有効かどうかを判定します。
有効でない場合、理由を説明する何かしらのエラーを返します。

入力に対して多態性を持たせるべきですが、本ケースでは`String`を受け取り`ValidationResult`を返す入力検証を振る舞いとして持つようにします。

```swift:Validator
protocol Validator {
    func validate(_ value: String) -> ValidationResult
}
```

事前準備として、個々のValidatorを実装する前に無効な入力に対して指定できるエラーを定義します。

```swift
enum EmailValidationError: Error {
    case empty
    case invalidFormat
}

enum PasswordValidationError: Error {
    case empty
    case tooShort
    case noUppercaseLetter
}
```

#### 個別のvalidator定義
ここから、詳細なバリデーションルールを定義していきます。

- 空文字チェック用のvalidator

```swift
struct EmptyStringValidator: Validator {
    private let invalidError: Error

    init(invalidError: Error) {
        self.invalidError = invalidError
    }

    func validate(_ value: String) -> ValidationResult {
        if value.isEmpty {
            return .invalid(errors: [invalidError])
        } else {
            return .valid
        }
    }
}
```

- メール形式に準拠しているかチェック用のvalidator

```swift
struct EmailFormatValidator: Validator {
    func validate(_ value: String) -> ValidationResult {
        let magicEmailRegexStolenFromTheInternet = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
        let emailTest = NSPredicate(format:"SELF MATCHES %@", magicEmailRegexStolenFromTheInternet)
        if emailTest.evaluate(with: value) {
            return .valid
        } else {
            return .invalid(errors: [EmailValidationError.invalidFormat])
        }
    }
}
```

ここではNSPredicateを使用していますが、簡易的な[Regex](https://gist.github.com/to4iki/10fdf4920fc100f1b3cbec02cfb376c0)クラス等を作成しマッチングを行っても良いです。

- パスワードの長さチェック用のvalidator

```swift
struct PasswordLengthValidator: Validator {
    func validate(_ value: String) -> ValidationResult {
        if value.characters.count >= 8 {
            return .valid
        } else {
            return .invalid(errors: [PasswordValidationError.tooShort])
        }
    }
}
```

- 大文字が含まれているかチェック用のvalidator

```swift
struct UppercaseLetterValidator: Validator {
    func validate(_ value: String) -> ValidationResult {
        let uppercaseLetterRegex = ".*[A-Z]+.*"
        let uppercaseLetterTest = NSPredicate(format:"SELF MATCHES %@", uppercaseLetterRegex)
        if uppercaseLetterTest.evaluate(with: value) {
            return .valid
        } else {
            return .invalid(errors: [PasswordValidationError.noUppercaseLetter])
        }
    }
}
```

#### validatorの合成
各validatorを組み合わせられるよう、任意数のvalidatorを梱包する複合validatorを定義します。
== 個々のvalidatorから大きなvalidatorの木構造を作成し、この木構造の葉のように使用できるようにする。


```swift:CompositeValidator
struct CompositeValidator: Validator {
    private let validators: [Validator]

    init(validators: Validator...) {
        self.validators = validators
    }

    func validate(_ value: String) -> ValidationResult {
        return validators.reduce(.valid) { (acc, validator) -> ValidationResult in
            switch validator.validate(value) {
            case .valid:
                return acc
            case .invalid(let errors):
                switch acc {
                case .valid:
                    return .invalid(errors: errors)
                case .invalid(let accErrors):
                    return .invalid(errors: accErrors + errors)
                }
            }
        }
    }
}
```

すべてのvalidatorを繰り返し処理し、有効な場合は以前の結果を返します。
無効な場合は、見つかった新しいエラーを以前のエラーと連結します。

#### Factoryクラス
本ユースケースに即したvalidatorを生成するためのファクトリークラスを定義します。

```swift:ValidatorFactory.swift
struct ValidatorFactory {
    static let sharedInstance = ValidatorFactory()
    private init() {}

    func emailValidator() -> Validator {
        return CompositeValidator(validators: emptyEmailStringValidator(), EmailFormatValidator())
    }

    func passwordValidator() -> Validator {
        return CompositeValidator(validators: emptyPasswordStringValidator(), passwordStrengthValidator())
    }

    private func emptyEmailStringValidator() -> Validator {
        return EmptyStringValidator(invalidError: EmailValidationError.empty)
    }

    private func emptyPasswordStringValidator() -> Validator {
        return EmptyStringValidator(invalidError: PasswordValidationError.empty)
    }

    private func passwordStrengthValidator() -> Validator {
        return CompositeValidator(validators: PasswordLengthValidator(), UppercaseLetterValidator())
    }
}
```

木構造のイメージ

![tree.png](https://qiita-image-store.s3.amazonaws.com/0/9303/dc9e7ffb-1e42-2f85-a0cc-154a59e032d1.png)

#### 使い方
では、実際に文字列を入力して検証してみましょう。

```swift
let emailValidator = ValidatorFactory.sharedInstance.emailValidator()
print(emailValidator.validate(""))
print(emailValidator.validate("invalidEmail@"))
print(emailValidator.validate("validEmail@validDomain.com"))

let passwordValidator = ValidatorFactory.sharedInstance.passwordValidator()
print(passwordValidator.validate(""))
print(passwordValidator.validate("psS$"))
print(passwordValidator.validate("paSSw0rd"))
```

標準出力

```swift
invalid([EmailValidationError.empty, EmailValidationError.invalidFormat])
invalid([EmailValidationError.invalidFormat])
valid

invalid([PasswordValidationError.empty, PasswordValidationError.tooShort, PasswordValidationError.noUppercaseLetter])
invalid([PasswordValidationError.tooShort])
valid
```

複数のエラー情報を統合して返すことが出来ました。

## まとめ
- 有用なフィードバックをユーザに提供するためには、複数のエラーを返すことが大事[^2]
- 小さなルール(部品)を組み合わせる[^3]ことで、大きな問題に立ち向かう
    - 複合Validatorを使用するこのパターンを実行し、アプリケーションが持つさまざまなニーズに合わせて柔軟にバリデーションチェックができる

## その他
今回紹介したやり方以外のvalidation実装を考えてみます。

#### 1. SwiftValidatorを使用する
Validatorライブラリは幾つか存在しますが、比較的スターが多くシンプルな[jpotts18/SwiftValidator](https://github.com/jpotts18/SwiftValidator)を触ってみます。

```swift
let validator = Validator()
validator.registerField(
    emailTextField, 
    errorLabel: emailErrorLabel,
    rules: [RequiredRule(), EmailRule(message: "Invalid email")]
)
```

実入力の値(String)を引数に受け取るのではなく、UITextLabelをバインドします(エラーメッセージ表示用のラベルも結びつけることが可能)。

予め、よく使用するであろう入力チェック(メール形式に準拠しているかなど)ルールが用意されているので、これらを柔軟に組み合わせることが可能ですね。
[SwiftValidator/Rules](https://github.com/jpotts18/SwiftValidator/tree/master/SwiftValidator/Rules)

#### 2. ScalazのValidation型を参考にする
[独習 Scalaz — Validation](http://eed3si9n.com/learning-scalaz/ja/Validation.html)
Validation型とは、複数のエラー値を保持できるデータ構造(Result型と同様にエラーと正常値を保持する直和型)。
雑に説明すると`Result<Value, [Error]>`です。

[Swiftz](https://github.com/typelift/Swiftz)に定義されていると思いきや無かったので、
[scalazのValidationの使い方　値の検証・エラー処理](http://tech-blog.tsukaby.com/archives/729)を参考に実装のイメージをしてみました。

```swift
enum Validation<E, A> {
    case Success(A)
    case Failure(E)
}

/// Nel == Non empty list
typealias ValidationNel<E, A> = Validation<NonEmptyList<E>, A>
```

```swift
struct Person {
    let name: String
    let age: Int
}

/// * これは実装イメージです
/// 値の検証とオブジェクトの生成を行う
func validatePerson(name: String, age: Int) -> ValidationNel<String, Person> {
    let validatedName: ValidationNel<String, String> = return name.isEmpty ? "名前が未入力です".failureNel : name.successNel
    let validatedAge: ValidationNel<String, Int> = return age < 18 ? "未成年です".failureNel : age.successNel
    return (validatedName • validatedAge)(Person.init)
}

switch validatePerson {
case .Success(a):
    print(a)
case .Failure(e):
    print(e)
}
```

上記の`validatePerson`メソッド内で失敗情報をNonEmptyListにaccumulateしていくことで検証を行っていきます。

こちらのイメージを具現化しようと実装に取り掛かったのですが、私の実装力不足で[WIP]状態です。。[to4iki/Validation](https://github.com/to4iki/Validation)
Swift2.2で書いていたのですが、今一度Swift3で再チャレンジしてみようと思います!![^4]

### See also
- [SwiftのvalidationライブラリSwiftValidators | フリップフラップ](http://blog.flup.jp/2016/03/05/swift_validators/)
- [Validation | Scalaz日本語ドキュメント](http://xuwei-k.github.io/scalaz-docs/validation.html)

[^1]: より実践的であるなら最低1文字以上の大文字、小文字、数字が含まれているのが好ましい
[^2]: ログイン時などセキュリティ上、パスワード失敗理由を教えすぎないようにするなど例外もある
[^3]: [UNIX philosophy: Make each program do one thing well](https://gist.github.com/koudaiii/82e4f8869705e5be065f#2-make-each-program-do-one-thing-well-1つのプログラムには1つのことをうまくやらせる)
[^4]: Generic Type Aliasesが使えるようになったので、表現しやすくなったはず [[Swift 3.0] 新しく追加されたGeneric Type Aliasesについて](http://dev.classmethod.jp/client-side/language-client-side/swift-3-generic-type-aliases/)