1. はじめに
会社で第5回の勉強会を主催したので学習内容のメモとなります。
条件分岐の解きほぐし方に関する勉強会の続きです。
2. 概要
今回のテーマは、条件分岐の解きほぐし方です。
今回の記事はポリシーパターンについて説明しています。
※解釈がずれている箇所がありましたら教えていただければと思います。
参考資料[1]:「良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方」
3. 場面設定
下図のような入力フォームのバリデーションを例に話を進めていきます。
仮にバリデーションの種類を以下の表のようにしたとします。
2行目までは共通処理で合致してそうですが、3行目は相違があることになります。
余談ですが、パスワードが長すぎるとハッシュ化するときにサーバの処理に負荷がかかりすぎることもあるらしく、Long password denial of service[2]というらしいです。
# | メール | パスワード |
1 | srting | string |
2 | Max 255文字以下 | Max 128文字以下[2] |
3 | 文字列@文字列.文字列の形 | Min 8文字以上 |
コードで普通に書いてみると以下のようになると思いますなんかいやですね。
問題
- 共通の判定処理が散在しやすい → 再利用したい
- バリデーションの種類増加=ネスト増加 → 見にくい
- 何のバリデーションなのかコードをみないとわからない → 名前設計
- 1か所の変更が他の判定処理に影響を与える可能性有 → 関心事の分離
まちがって他の箇所に変更加えていないかとか
const pass = "12345678";
if(typeof pass === "string") {
if(pass.length >= 8) {
if(pass.length <= 128) {
console.log("OK!");
}
}
}
4 ポリシーパターンで改善
それでは条件分岐をポリシーパターンで解きほぐしていきます。
4.1 共通処理をまとめる
処理は違えどバリデーションという共通処理。
// パスワード
const pass = "12345678";
if(typeof pass === "string") {
if(pass.length <= 128) {
if(pass.length >= 8) {
// メールアドレス
const mail = "test@test.com";
if(typeof mail === "string") {
if((mail.length <= 255) {
if(regularexpression(mail)) { // 正規表現でフォーマットを確認
バリデーションの結果を返すokメソッドで抽象化してみます。
interface Validation {
ok(target : string) : boolean;
}
Validationの具体的な処理を実装。
Validationを実装するサブクラスに処理の関心事を分離することができた。
- 1か所の変更が他の判定処理に影響を与える可能性有 →
関心事の分離解決
class MaxLen implements Validation{
private readonly MAX_LEN;
constructor(length : number) {
this.MAX_LEN = length;
}
ok(target: string): boolean {
if(target.length <= this.MAX_LEN) {
return true;
}
console.log("最大文字列の違反");
return false;
}
}
4.2 項目ごとにバリデーションを管理する
メールアドレス、パスワードを1つの管理対象としSetで管理してみる。
ネストも浅くなって見やすくなりました。
- バリデーションの種類増加=ネスト増加 →
見にくい解決
const mail = new FormValidation();
console.log(mail.validationMail("test@test.com")); // false 最大文字列の違反
/////////////////////////////////////////////////////
class FormValidation {
private readonly mailRules = new Set<Validation>();
constructor() {
this.mailRules.add(new MailFormat()); // メールフォーマット
this.mailRules.add(new MaxLen(10)); // 最大文字列
this.mailRules.add(new InputRequired()); // 入力必須
}
public validationMail(validationTarget : string) {
for(const rule of this.mailRules) {
if(!rule.ok(validationTarget)) {
return false;
}
}
return true;
}
}
処理の動きを表すのであればこのような動きでしょうか。

このように、メールとパスワードでSetを分けたとして共通の判定処理を再利用。
それぞれ、なんのバリデーションをかけているのかもインスタンス名からわかります。
- 共通の判定処理が散在しやすい →
再利用したい解決 - 何のバリデーションなのかコードをみないとわからない →
名前設計解決
class FormValidation {
private readonly mailRules = new Set<Validation>();
private readonly passRules = new Set<Validation>();
constructor() {
// メール
this.mailRules.add(new MailFormat()); // メールフォーマット
this.mailRules.add(new MaxLen(10)); // 最大文字列
this.mailRules.add(new InputRequired()); // 入力必須
// パスワード
this.passRules.add(new MinLen(8)); // パスワード固有
this.passRules.add(new MaxLen(125)); // 共通
this.passRules.add(new InputRequired()); // 共通
}
}
5. 実際に実装するのか
おそらく実装しない人も多いのではないでしょうか。
ライブラリやフレームワークで実装されてるから作る必要がないことも多いと思います。
例:Yup Zod Laravel
こちらはLaravelのバリデーションの例です。
やってることはtypescriptの例と同じですね。
public function post(Request $request) {
$rulus = [
'name' => 'required', // ルールからバリデーションの配列を作成
'age' => 'integer | between:0,150',
];
$message = [
'name.required' => '名前を入力してください',
'age.numeric' => '整数で入力してください',
'age.between' => '0~150で入力してください'
];
// 配列回してfalseなら終了
$validator = Validator::make($request->all(), $rulus, $message);
if ($validator->fails()) {
return redirect('/hello')->withErrors($validator)
}
return view('hello.index',['msg'=>'正しく入力されました!']);
}
6. まとめ
今回のゴール:ポリシーパターンで条件分岐をスマートに書く方法を知る。
条件判定の役割を1クラスそのものが担うことができる。
ポリシーパターンを自分で書くことは少ない。
ライブラリやフレームワークの内部処理として実装されてるかも。
下記問題が解決できる
- 共通の判定処理が散在しやすい → 再利用したい
- バリデーションの種類増加=ネスト増加 → 見にくい
- 何のバリデーションなのかコードをみないとわからない → 名前設計
- 1か所の変更が他の判定処理に影響を与える可能性有 → 関心事の分離
7. 参考文献
- 良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方
- ”OWASP,” https://github.com/OWASP/ASVS/blob/master/4.0/en/0x11-V2-Authentication.md#v21-password-security, (Access. 2024/2/18).