どんなシステムでもバリデーションは必要ですよね。
しかし、あまり深く考えたことはなく、チームの方針や既存コードのコピペで済ませていることが大半だと思います。
今回独りwebサービスの作成で色々と考えることが出来ましたので共有。
前提
・基本的にはwebサービスのAPI側(考え方はフロントでもwinFormsでも一緒だと思います)
・min,max,between,リスト照合等
・汎用的に実装したいが個別でロジックが必要な箇所もある
また英語苦手なので単語の意味を再定義すると、
Validation:「検証」を意味する名詞
validate:「検証する」を意味する動詞
valid:「正しい」や「検証された」を意味する形容詞
Validationの種類
開発においてValidationには大きく2つの類型があると肌感覚ですが考えています。
本当は正しい名称があるのかもしれませんが記事中は以下の2つと定義します。
全体バリデーション:検証を一括で行い全体として正常か確認する。
個別バリデーション:各入力欄それぞれを検証する。
全体バリデーションは「登録」等のボタンを押下した際に判定するものです。
ユーザ情報入力フォーム等、一括で検証された方がストレスのない場面にて実装されます。
また、個別バリデーションを通した後でも最終確認の意味で再度実行することもよくあります。
個別バリデーションは各入力欄が入力された後(Enter等)で実行されます。
郵便番号のような、他の入力にも影響する場合や、ユニークキー情報に関する情報を入力する場面では必要となってきます。
実務においてはこの2つを組み合わせて入力者がストレスに感じない、かつ安全な仕組みを作っていきます。
エラー表示
バリデーションにエラーがあった場合、結果を適切にユーザに表示することは最優先です。
勿論例外発生等も適切なエラー表示が無ければ、ユーザもエンジニアも無駄に疲弊してしまいます。
このエラー表示が不親切だとユーザ側で切り分けが出来ず、問い合わせコストが甚大になってしまいます。軽視されがちですが、ここが最初から練り上げられているシステムはとても保守しやすくなります。
このエラー表示にも種類があり、
・個別エラー表示
・リストエラー表示
・例外エラー表示
この3タイプに分けられると考えています。
個別エラー表示は恐らく一番良くユーザが見るエラーです。
各入力欄の下に表示されるエラーやfetch処理が失敗した際のエラー等、まさに個別の現象に関するエラーを示します。
リストエラー表示はループ処理やリスト処理が絡んでくる場合に必要となってくるエラーです。
100行のデータを取り込む際に全ての行にエラーがあった場合、
個別エラーだと「行ID:n 担当者CDがエラーです。」を100行分通知することになってしまいます。
もしくは「取込に失敗しました。」とだけ表示するものの、何が原因なのかわからないという状態。
一方、リストエラー表示だと、
取込エラーが発生しました。
行ID:1 担当者CDがエラーです。
行ID:2 担当者CDがエラーです。…
という風にまとめてエラーを1つの通知で表示することが可能です。
実務ではこの実装が面倒で個別エラータイプで実装されていることが良くありますが、保守やユーザビリティも含めると是非リストエラー表示は行いたいです。
例外エラー表示はソリューションの属性(外部に公開しているか)等によっても最適な形は変わってきます。
オンプレなシステムの場合、ユーザ向けのエラーの文言を表示すると同時にスタックトレースも張るようにすればかなり保守しやすいです。
一方外部に公開しているシステムであればそんなわけにもいかないので、エラー文言とエラーコードを返しながら、保守用ログをDBもしくはサーバ内にテキストファイルとして吐き出し、ログ番号も付加して通知するような設計が良いかと思っています。
Validationの実装を行う際には特に、
個別エラー表示とリストエラー表示どちらが良いのか?
も一緒に考えておいてください。
どのように行う?
さてこのValidationは類型を考慮すると同じ画面や機能でも複数の個所&タイミングで呼び出されることが考えられます。
また、基本的なValidationは勿論、リスト照合系や動的な検証も統一した処理を作成し同じものを呼び出したいです。
そうなるとどこで実装するのが一番良いのでしょうか?
まず、最終防衛ラインであるDB側での実装は必須です。といっても出来ることは限られますが、
例えば適切な桁数を設定することでそれ以上の値が入ることを防げます。
例:NVARCHAR(10)
では、APIやwinFormsで利用する場合のことを考えてみましょう。
C#においてValidationを実装する際には幾つか方法があります。
・Attrivuteにて実装する
・IValidatableObjectを継承したModelを作成する
・コードで地道に実装
幾つかありますが、個人的には「コードで地道に実装」が一番だと思ってます。
Attrivuteで実装するとコントローラのメソッドのパラメータ部分で実装出来たりと確かにコード削減は可能ですが、静的な定義しかできないため、例えば他の入力欄によって正しい値が変わる場合や、DBのデータによる検証が必要な場合に対処できません。
また、IValidatableObjectを継承するパターンでもDIが難しいことやModelに責任が集中しすぎてしまう等で将来的に設計が破綻する可能性もあります。
そうなると「コードで地道に実装」するのが遠回りに見えて一番現実的だったりします。
どこに・どのように書く?
先程の話も繋がりますが、責務の分離がキーワードになってきます。
本来、原理原則で言えばValidationはそれ単体で責任を持つ作業であり、UIやController、ドメイン層から分離され独自で行われている方が正しい設計と言えるでしょう。
しかし現実と照らし合わせると、各機能によってValidationに求められる性質は内容もタイミングも期待されるエラー値も違ってきます。
UIやAPIパラメータといったユーザとの接点では「理解できる(呼び出し者で対処できる)形でのエラー内容」が返ってくる必要がありますし、ドメインロジックでは「上位で担保されていて欲しい入力値のエラー」に加え「ドメイン上の不整合を説明するためのエラー内容」が返ってくる必要があります。
これらのことから、自分は
・プレゼンテーション層(UI [VM] or Controller)
・ドメインロジック層(Service)
この2つで検証するスタイルをとっています。特に要はドメインロジック層です。
検証するためのコードは普遍的なものをCommonValidator、Modelと結びつく普遍的なものは形でModel名Validator(主にCommonValidatorのラッパー)、また、enumに関するもの等はEnumValidator、その他一時的なに使われる特別なものはその必要なクラス内に記述という風に分けています。
こうすることで、例えばclientsというテーブルのidを外部キーとして利用している他のmodelでもClientValidator.IsValidId(id)みたいな形で共通して呼び出せますし、UIとドメイン層で柔軟に結果を利用することが出来ます。
この設計だと通常処理系統でも同じValidationを2回クリアする可能性がありますが、出国検査と入国審査みたいな感じで「同じようなことだけど担保したいことや目的が違う」という風に考えてます。
また、validate結果はValidResultというクラスを返すようにしています。こうすることで将来的な拡張性や実装の標準化も担保。
まとめ
これは個人的な開発Tipsであり万人に通じるものとは思っていませんが、Validationはログイン処理やセッション管理レベルで考えるべき概念だと思っています。
今一度、自分達の最適なValidationはどんなものなのか考えてみてはどうでしょうか。