Functional Validationパターン
1. 目的
バリデーションロジックをいい感じに管理したい
- バリデーションロジックをひとつの関数にまとめると保守性に難がある
- 極力シンプルな方法でバリデーションロジックの管理と実装をしたい
2. 課題
ひとつの関数に複数のバリデーションロジックを記述したくない
- ↓のようにひとつの関数内に複数のバリデーションロジックを詰め込むのは保守性やテスト容易性の観点から好ましくない
def example(value):
result = []
if len(value.get('name', '')) == 0:
result.append({
"message": "名前を入力してください"
})
if len(value.get('name', '')) > 127:
result.append({
"message": "名前は127文字以内で入力してください"
})
if len(value.get('age', 0)) <= 17:
result.append({
"message": "18歳未満はご利用頂くことができません"
})
# 以下、個別項目の入力チェックが延々と続く
...
return result
3. 解決策
バリデーションロジックは関数に分割し、ループで実行する
value = {
'name': 'Jhon Doe',
'age': 17
}
- このデータを検証するロジックを個別に関数化する
- 返り値は同じ形式の辞書(またはオブジェクト)を返すようにする
def check_001(value):
if len(value.get('name', '')) == 0:
return {
"message": "名前を入力してください"
}
def check_002(value):
if len(value.get('name', '')) > 127:
return {
"message": "名前は127文字以内で入力してください"
}
def check_003(value):
if value.get('age', 0) <= 17:
return {
"message": "18歳未満はご利用頂くことができません"
}
- バリデーションロジックを実行する関数(
validate
)はバリデーション対象のデータとバリデーションロジックを引数に取る
-
validate
はバリデーションロジックを複数受け取れるように可変長引数とする
- 可変長引数はiterableなので、for文に渡すことができる
- サンプルコードはわかりやすさ優先でfor文で書いたが、リスト内包記法やmap関数を用いてもよい
def validate(value, *functions):
result = []
for f in functions:
validate_result = f(value)
if not validate_result is None:
result.append(validate_result)
return result
value = {
"name": "Jhon Doe",
"age": 17
}
result = validate(
value,
check_001,
check_002,
check_003,
)
print(result) # <= [{'message': '18歳未満はご利用頂くことができません'}]
- サンプルコードはPythonで書いたが、関数オブジェクト/可変長引数/map関数を備える言語ならどのような言語でも実装可能である(昨今の言語は大抵満たしている)
4. メリット
バリデーションロジックを個別に管理できる
- バリデーションロジックを関数単位で個別に管理することができる
- バリデーションロジックが個別に関数化されることで個別のバリデーションロジックの実装やユニットテストの作成コストを低下させることができる
バリデーションロジックを自由に組み合わせることができる
- バリデーションロジックを任意に組み合わせて実行できるので、ひとつの関数に全てのロジックが記述される形式と比較すると柔軟性が高い
- バリデーションロジックの重複も抑制することができる
5. デメリット
関数が増える
- メリットの裏返しになるが、バリデーションロジックを個別に関数化する為、関数の数が増加してしまう
- それぞれの関数に適切な名前を与え、実装やテストを行うのは当然ながら手間がかかる
- 長期的にはメリットがデメリットを上回ると考えられるが、初期の実装速度は低下することが予測される
6. 注意
目的に見合ったバリデーションライブラリが存在するならそちらを使う
- 最近は優れたバリデーションライブラリも多いので、あえてバリデーションロジックを再実装する必要はないという考え方にも一理ある
- 単純なフォーマットチェックであれば、フレームワーク組み込みのバリデーション機構やサードパーティのバリデーションライブラリを使った方がよい(情報量やサンプルの蓄積の面で優位性がある)
- 特殊なバリデーションロジックが必要になった場合に使用を検討するとよい
移植性/再利用性はそこまで高くはない
- バリデーションロジックはその性質上、特定のデータと強く結合するものである
- したがって、あるデータの為に作られたバリデーションロジックを別のデータの為に転用することは原則不可能だと考えた方がよい
参考資料
Appendix-1 ソースコード全体
def validate(value, *functions):
result = []
for f in functions:
validate_result = f(value)
if not validate_result is None:
result.append(validate_result)
return result
def check_001(value):
if len(value.get('name', '')) == 0:
return {
"message": "名前を入力してください"
}
def check_002(value):
if len(value.get('name', '')) > 127:
return {
"message": "名前は127文字以内で入力してください"
}
def check_003(value):
if value.get('age', 0) <= 17:
return {
"message": "18歳未満はご利用頂くことができません"
}
if __name__=="__main__":
value = {
"name": "Jhon Doe",
"age": 17
}
result = validate(
value,
check_001,
check_002,
check_003,
)
print(result) # <= [{'message': '18歳未満はご利用頂くことができません'}]