バリデーションを分類して、段階を分けて実行する
サービスが大きくなってくると、フォームに入力できる項目が多くなってきます。そうすると、長さや範囲などの単純なバリデーションだけでなく、項目間のチェックを行う必要がでてきます。また、状態遷移などを扱うようになると、現在DBに登録されている値と入力値を見比べる必要も出てきます。
それらをいっぺんにチェックしようとすると、コードがごちゃごちゃしやすく、バグが生まれやすくなります。ここではバリデーションをいくつかのステップに分けて実行する方法を提案します。
(注: どちらかというと、サーバーサイドでのバリデーションを想定しています)
サンプルアプリ:会議室予約システム
例として、会議室予約システムを考えましょう。
入力項目は、
- 会議名
- 会議室
- 参加人数
- 開始日時
- 終了日時
です。
バリデーション
このサンプルアプリケーションのバリデーションは以下です。
- 会議名は必須で3文字以上256文字以下
- 会議室は必須で1以上の数、存在する会議室であること
- 参加人数は必須、1以上50以下、会議室のキャパシティを超えないこと
- 開始日時は必須。登録時は現在日時より後であること。開始日時を変更時、過去に変更できない。開始日時を過ぎていたら、開始日時は変更できない
- 終了日時は必須。登録時は現在日時より後であること。終了日時を変更時、過去に変更できない。終了日時を過ぎていたら、終了日時を変更できない
- 開始日時は終了日時より前、開始日時から終了日時は15分以上2時間以下
- 1つの会議室で、会議は重複して取れない
バリデーションの分類と順番
バリデーションを分類します。
Step1. 単一フィールドを対象にしたバリデーション(外部接続なし)
必須チェック、文字列の長さチェック、数値の範囲チェックなどをします。
外部接続なしとは、DBやAPIに問い合わせて有効な値かどうかはここではチェックしないということです。
サンプルアプリケーションでは、以下のバリデーションが該当します。
- 会議名は必須で3文字以上256文字以下
- 会議室は必須で1以上の数
- 参加人数は必須、1以上50以下
- 開始日時は必須。登録時は現在日時より後であること
- 終了日時は必須。登録時は現在日時より後であること
Step2. フィールド間のバリデーション(外部接続なし)
時間の前後関係や条件付き必須などをチェックします。ここでもDBやAPI接続を用いたチェックは行いません。
サンプルアプリケーションでは、以下のバリデーションが該当します。
- 開始日時は終了日時より前、開始日時から終了日時は15分以上2時間以下
Step3. 登録前の値を用いたチェック(外部接続なし)
現在DBに登録されている値を用いたチェックをします。状態遷移や変更の検知などのバリデーションが該当します。
サンプルアプリケーションでは、以下のバリデーションが該当します。
- 開始日時を変更時、過去に変更できない。開始日時を過ぎていたら、開始日時は変更できない
- 終了日時を変更時、過去に変更できない。終了日時を過ぎていたら、終了日時を変更できない
Step4. 外部への接続を用いたチェック
外部への接続(DBやAPI)を用いたチェックをします。例えば、郵便番号が存在するか、郵便番号APIを使って確かめます。ユニークチェックなどもここに含まれます。
サンプルアプリケーションでは、以下のバリデーションが該当します。
- 会議室は存在する会議室であること
- 参加人数は会議室のキャパシティを超えないこと
- 1つの会議室で、会議は重複して取れない
シーケンス図
シーケンス図です。初めに入力値(inputModel)だけを対象にStep1のバリデーション(validateSimple)を行います。エラーがあれば処理を中断します。そのあと、DBから現在登録されている値を取得し、入力値で上書きします(merge)。マージされたモデルをもとにStep2のバリデーションを行います(validateFieldCorrelation)。ここでエラーがあれば処理を中断します。さらにStep3のバリデーション(validateWithDbModel)、Step4のバリデーション(validateNonFunctional)を行い、それぞれエラーがあればそこで処理を中断します。
まとめ
バリデーションがごちゃごちゃしてきたら落ち着て分類してみてねというお話でした。
Q&A
できるだけ多くのエラーをユーザに見せたほうがよいのでは。この実装だとユーザは最大4回もリクエストを送らなければいけない
なるべく多くのエラーを見せたほうがいいのはその通りです。そのようにできれば理想です。
しかしそれを実現しようとすると、バグが多くなる、というのが実感です。
例えば、上記のStep1からStep4を同時にしようとすると、そもそもおかしな値が入っているのにDBに問い合わせを行おうとしたりします。それを防ぐためにコードを余計に書くことになり、その部分にバグが混入しやすいのです。
また、ユーザは多くの入力ミスはしないという前提に立っています。上記の例では会議室の存在チェックを行っていますが、これはおかしな値が入るのを防ぐ役割であって、通常のユーザであれば会議室の番号は間違えないのではないかと思います。
ソースコード
サンプルアプリケーションをJava/SpringBootにて実装しています。参考にどうぞ。なお、APIのみで画面はありません。
Discussion