Featured image of post ネストしたif文に埋もれた検証ルールを、1ルール1クラスに分けて鎖でつなぐ【Chain of Responsibility】

ネストしたif文に埋もれた検証ルールを、1ルール1クラスに分けて鎖でつなぐ【Chain of Responsibility】

ネストした validate_user を、Moo の Validator を継承する RequiredValidator や EmailValidator など、1つの検証ルールだけを受け持つクラスに分けます。set_next で順につなぎ、先頭の validate を呼ぶと入力がすべてのバリデータを通ってエラーが1つのハッシュに集まる、Chain of Responsibility パターンの形にします。禁止ワードのチェックは、クラスを1つ作ってチェーンに1つつなげば加えられます。

@nqounetです。

「ユーザー登録バリデーションを学ぶ」シリーズの第3回(最終回)です。

前回は、検証ルールが増えることでif文がネスト化し、保守困難なコードになる問題を体験しました。

今回は、その問題を「Chain of Responsibility」パターンで解決します。各検証ルールを独立したクラスに分離し、柔軟に組み合わせられる設計に変更していきましょう。

Chain of Responsibilityパターンとは

Chain of Responsibility(責任の連鎖)パターンは、GoF(Gang of Four)の23のデザインパターンの1つです。

このパターンでは、リクエストを処理するハンドラを連鎖(チェーン)状につなぎ、各ハンドラが自分の責任範囲を処理します。自分で処理できない場合は、次のハンドラに処理を委譲します。

	flowchart LR
    Input["入力データ"] --> V1["必須チェック"]
    V1 --> V2["形式チェック"]
    V2 --> V3["長さチェック"]
    V3 --> Result["結果"]

バリデーションでは、各検証ルールをハンドラとして実装し、チェーンでつなぐことで柔軟な検証フローを構築できます。

パターン適用のメリット

前回の問題コードと比較して、Chain of Responsibilityパターンには以下のメリットがあります。

問題パターン適用後
ネストが深い各ハンドラはフラットな構造
依存関係が暗黙的チェーン構築で明示的に表現
新ルール追加が困難新しいハンドラを追加するだけ
テストが困難各ハンドラを独立してテスト可能
修正の影響範囲が不明確ハンドラごとに影響が局所化

基底Validatorクラスの設計

まず、すべてのバリデータの基底となるクラスを設計します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# validator_base.pl
# Perl v5.36+, Moo

use v5.36;
use utf8;
use warnings;
binmode STDOUT, ':utf8';

package Validator {
    use Moo;

    # 次のハンドラへの参照
    has next_handler => (
        is        => 'rw',
        predicate => 'has_next_handler',
    );

    # 対象フィールド名
    has field_name => (
        is       => 'ro',
        required => 1,
    );

    # エラーメッセージ
    has error_message => (
        is       => 'ro',
        required => 1,
    );

    # 前提となるフィールド(そこにエラーがあれば検証しない)
    has depends_on => (
        is        => 'ro',
        predicate => 'has_depends_on',
    );

    # 次のハンドラを設定し、そのハンドラを返す(チェーン構築用)
    sub set_next ($self, $handler) {
        $self->next_handler($handler);
        return $handler;
    }

    # バリデーション実行
    sub validate ($self, $input, $errors) {
        # depends_on のフィールドにエラーがなければ _do_validate で検証し、
        # 失敗したら $errors ハッシュに追加(同じフィールドは最初のエラーだけ残す)

        # 次のハンドラがあれば委譲
        if ($self->has_next_handler) {
            return $self->next_handler->validate($input, $errors);
        }

        return $errors;
    }

    # 具体的な検証ロジック(サブクラスで実装)
    sub _do_validate ($self, $value, $input) {
        die "サブクラスで_do_validateを実装してください";
    }
};

1;

基底クラスのポイントは以下のとおりです。

  • next_handler - 次のハンドラへの参照を保持する
  • field_name - 検証対象のフィールド名を保持する
  • error_message - エラー時のメッセージを保持する
  • depends_on - 前提となるフィールドを保持する(省略可)。そのフィールドにすでにエラーがあれば、このハンドラは検証せずに次へ渡す
  • set_next() - チェーン構築のためのメソッド
  • validate() - 検証を実行し、次のハンドラに委譲する。同じフィールドにエラーがすでにあれば上書きしない(チェーンで先に並べたルールのエラーが残る)

具体的なバリデータの実装

基底クラスを継承して、具体的なバリデータを実装します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# validators.pl
# Perl v5.36+, Moo

use v5.36;
use utf8;
use warnings;
binmode STDOUT, ':utf8';

# 基底Validatorクラス
package Validator {
    use Moo;

    has next_handler => (
        is        => 'rw',
        predicate => 'has_next_handler',
    );

    has field_name => (
        is       => 'ro',
        required => 1,
    );

    has error_message => (
        is       => 'ro',
        required => 1,
    );

    has depends_on => (
        is        => 'ro',
        predicate => 'has_depends_on',
    );

    sub set_next ($self, $handler) {
        $self->next_handler($handler);
        return $handler;
    }

    sub validate ($self, $input, $errors) {
        my $value = $input->{$self->field_name} // '';

        # 前提のフィールドにエラーがあれば、検証せずに次へ
        my $skip = $self->has_depends_on && exists $errors->{$self->depends_on};

        # 検証実行(サブクラスの_do_validateを呼ぶ)
        if (!$skip && !$self->_do_validate($value, $input)) {
            # 同じフィールドにエラーがなければ追加
            $errors->{$self->field_name} //= $self->error_message;
        }

        # 次のハンドラがあれば委譲
        if ($self->has_next_handler) {
            return $self->next_handler->validate($input, $errors);
        }

        return $errors;
    }

    sub _do_validate ($self, $value, $input) {
        die "サブクラスで_do_validateを実装してください";
    }
};

# 必須チェック
package RequiredValidator {
    use Moo;
    extends 'Validator';

    sub _do_validate ($self, $value, $input) {
        return $value ne '';
    }
};

# メールアドレス形式チェック
package EmailValidator {
    use Moo;
    extends 'Validator';

    sub _do_validate ($self, $value, $input) {
        # 空の場合はスキップ(必須チェックは別のバリデータで行う)
        return 1 if $value eq '';
        return $value =~ /\A[^@\s]+\@[^@\s]+\.[^@\s]+\z/;
    }
};

# 文字列長チェック
package LengthValidator {
    use Moo;
    extends 'Validator';

    has min_length => (is => 'ro', default => 0);
    has max_length => (is => 'ro');

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';
        my $len = length($value);
        return 0 if $len < $self->min_length;
        return 0 if defined $self->max_length && $len > $self->max_length;
        return 1;
    }
};

# 正規表現チェック
package PatternValidator {
    use Moo;
    extends 'Validator';

    has pattern => (is => 'ro', required => 1);

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';
        return $value =~ $self->pattern;
    }
};

# 確認一致チェック
package ConfirmValidator {
    use Moo;
    extends 'Validator';

    has confirm_field => (is => 'ro', required => 1);

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';  # 空かどうかは RequiredValidator で見る
        my $original = $input->{$self->confirm_field} // '';
        return $value eq $original;
    }
};

# 真偽値チェック(利用規約同意など)
package BooleanValidator {
    use Moo;
    extends 'Validator';

    sub _do_validate ($self, $value, $input) {
        return $value ? 1 : 0;
    }
};

# 数値範囲チェック
package RangeValidator {
    use Moo;
    extends 'Validator';

    has min_value => (is => 'ro');
    has max_value => (is => 'ro');

    sub _do_validate ($self, $value, $input) {
        return 1 if !defined $value || $value eq '';
        return 0 unless $value =~ /\A[0-9]+\z/;
        return 0 if defined $self->min_value && $value < $self->min_value;
        return 0 if defined $self->max_value && $value > $self->max_value;
        return 1;
    }
};

1;

各バリデータはシンプルで、自分の責任範囲だけを担当します。これが「単一責任の原則」です。

チェーンの構築と実行

バリデータを組み合わせてチェーンを構築し、ユーザー登録の検証を行います。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# チェーン構築の例
sub build_user_registration_chain () {
    # 各バリデータを作成
    my $name_required = RequiredValidator->new(
        field_name    => 'name',
        error_message => '名前を入力してください',
    );

    my $email_required = RequiredValidator->new(
        field_name    => 'email',
        error_message => 'メールアドレスを入力してください',
    );

    my $email_format = EmailValidator->new(
        field_name    => 'email',
        error_message => 'メールアドレスの形式が正しくありません',
    );

    my $password_required = RequiredValidator->new(
        field_name    => 'password',
        error_message => 'パスワードを入力してください',
    );

    my $password_length = LengthValidator->new(
        field_name    => 'password',
        error_message => 'パスワードは8文字以上で入力してください',
        min_length    => 8,
    );

    my $password_upper = PatternValidator->new(
        field_name    => 'password',
        error_message => 'パスワードには大文字を含めてください',
        pattern       => qr/[A-Z]/,
    );

    my $password_lower = PatternValidator->new(
        field_name    => 'password',
        error_message => 'パスワードには小文字を含めてください',
        pattern       => qr/[a-z]/,
    );

    my $password_digit = PatternValidator->new(
        field_name    => 'password',
        error_message => 'パスワードには数字を含めてください',
        pattern       => qr/[0-9]/,
    );

    # パスワードにエラーがないときだけ、確認用を検証する
    my $confirm_required = RequiredValidator->new(
        field_name    => 'password_confirm',
        error_message => '確認用パスワードを入力してください',
        depends_on    => 'password',
    );

    my $password_confirm = ConfirmValidator->new(
        field_name    => 'password_confirm',
        error_message => 'パスワードが一致しません',
        confirm_field => 'password',
        depends_on    => 'password',
    );

    my $agreed_terms = BooleanValidator->new(
        field_name    => 'agreed_terms',
        error_message => '利用規約に同意してください',
    );

    my $age_required = RequiredValidator->new(
        field_name    => 'age',
        error_message => '年齢を入力してください',
    );

    my $age_number = PatternValidator->new(
        field_name    => 'age',
        error_message => '年齢は数値で入力してください',
        pattern       => qr/\A[0-9]+\z/,
    );

    my $age_range = RangeValidator->new(
        field_name    => 'age',
        error_message => '18歳以上の方のみ登録できます',
        min_value     => 18,
    );

    # チェーンを構築
    $name_required
        ->set_next($email_required)
        ->set_next($email_format)
        ->set_next($password_required)
        ->set_next($password_length)
        ->set_next($password_upper)
        ->set_next($password_lower)
        ->set_next($password_digit)
        ->set_next($confirm_required)
        ->set_next($password_confirm)
        ->set_next($agreed_terms)
        ->set_next($age_required)
        ->set_next($age_number)
        ->set_next($age_range);

    return $name_required;  # チェーンの先頭を返す
}

# バリデーション実行
sub validate_user ($input) {
    my $chain = build_user_registration_chain();
    my $errors = {};

    $chain->validate($input, $errors);

    if (%$errors) {
        return { ok => 0, errors => $errors };
    }

    return { ok => 1, data => $input };
}

チェーン構築のポイントは以下のとおりです。

  • 各バリデータは独立したオブジェクトとして作成する
  • set_next() でバリデータをつなげてチェーンを構築する
  • チェーンの先頭から validate() を呼ぶと、すべてのバリデータが順に実行される
  • 同じフィールドのルールは、並べた順に優先される。たとえばパスワードは「8文字以上」→「大文字」→「小文字」→「数字」の順に並べてあるので、最初に引っかかったルールのエラーだけが残る
  • 確認用パスワードの2つのバリデータには depends_on => 'password' を渡している。前回のコードでは「パスワードが条件を満たしたときだけ確認用を検証する」という依存が if/else のネストの中に埋もれていたが、ここではチェーンを組み立てる場所に書いてある。depends_on はエラーが記録済みかどうかを見るので、パスワードのバリデータは確認用より前に並べておく

ここまでの組み立てで、前回のコードと同じ入力に同じエラーが返ります(記事末尾の完成コードで、前回の5つのテストケースを同じ順に流して確かめています)。

拡張性の実証

新しい検証ルールを追加してみましょう。たとえば「ユーザー名に禁止ワードを含まない」というルールを追加する場合、既存のバリデータのクラスには手を入れずに済みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 禁止ワードチェック(新規追加)
package ForbiddenWordsValidator {
    use Moo;
    extends 'Validator';

    has forbidden_words => (
        is      => 'ro',
        default => sub { ['admin', 'root', 'system'] },
    );

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';
        my $lower = lc($value);
        for my $word ($self->forbidden_words->@*) {
            return 0 if index($lower, $word) >= 0;
        }
        return 1;
    }
};

この新しいバリデータをチェーンに追加するだけで、検証ルールが拡張されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 既存のチェーンに追加
my $name_forbidden = ForbiddenWordsValidator->new(
    field_name    => 'name',
    error_message => '使用できない文字列が含まれています',
);

$name_required
    ->set_next($name_forbidden)  # 新しいバリデータを追加
    ->set_next($email_required)
    # ... 以下同様

build_user_registration_chain で手を入れるのは、新しいバリデータを作ってチェーンにつなぐ数行だけです。既存のバリデータを変更せずに新しいルールを足せる、という意味で「開放閉鎖原則」に沿った形になっています。

シリーズの振り返り

全3回のシリーズを振り返ってみましょう。

回学んだこと
第1回if文でシンプルなバリデーションを実装
第2回検証ルール追加でコードが複雑化する問題を体験
第3回Chain of Responsibilityパターンで問題を解決

最初はシンプルなif文から始まり、要件追加でコードが複雑化し、最後にデザインパターンで解決する。この流れを体験することで、「なぜパターンが必要なのか」を実感できたのではないでしょうか。

Chain of Responsibilityパターンの本質

このパターンの本質は「責任の分離」と「柔軟な組み合わせ」です。

  1. 責任の分離 - 各バリデータは1つの検証ルールだけを担当する
  2. 柔軟な組み合わせ - チェーン構築で検証フローを自由に変更できる
  3. 拡張性 - 新しいルールは、既存のバリデータを変更せずにクラスを1つ足し、チェーンに1つつなぐだけで加えられる
  4. テスト容易性 - 各バリデータを独立してテスト可能

次のステップへ

Chain of Responsibilityパターンを学んだ皆さんは、以下のステップに進んでみてください。

  • テストを書く - 各バリデータに対して単体テストを書いてみる
  • 他のデザインパターン - Singletonパターンなど、他のGoFパターンを学ぶ
  • Moo::Role - 共通インターフェースをRoleで定義する

デザインパターンを学ぶことで、より良い設計ができるようになります。

関連記事

前提知識

他のGoFパターン

「次のステップへ」で挙げたSingletonパターンの回です。

インターフェース設計

まとめ

  • Chain of Responsibilityパターンは、ハンドラをチェーン状につなぐデザインパターンである
  • 各バリデータは1つの検証ルールだけを担当し、責任が分離される
  • set_next() でチェーンを構築し、柔軟に検証フローを組み立てられる
  • 新しいルールは、既存のバリデータを変更せず、チェーンに1つつなぐだけで加えられる(開放閉鎖原則)
  • 各バリデータを独立してテストできる

完成コード

この回の完成コードを1つのスクリプトにまとめました。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#!/usr/bin/env perl
# form-validation-03.pl
# ユーザー登録バリデーション(Chain of Responsibilityパターン版)
# Perl v5.36+, Moo

use v5.36;
use utf8;
use warnings;
use Moo;
binmode STDOUT, ':utf8';

# ===== 基底Validatorクラス =====
package Validator {
    use Moo;

    has next_handler => (
        is        => 'rw',
        predicate => 'has_next_handler',
    );

    has field_name => (
        is       => 'ro',
        required => 1,
    );

    has error_message => (
        is       => 'ro',
        required => 1,
    );

    has depends_on => (
        is        => 'ro',
        predicate => 'has_depends_on',
    );

    sub set_next ($self, $handler) {
        $self->next_handler($handler);
        return $handler;
    }

    sub validate ($self, $input, $errors) {
        my $value = $input->{$self->field_name} // '';

        # 前提のフィールドにエラーがあれば、検証せずに次へ
        my $skip = $self->has_depends_on && exists $errors->{$self->depends_on};

        if (!$skip && !$self->_do_validate($value, $input)) {
            # 同じフィールドにエラーがなければ追加
            $errors->{$self->field_name} //= $self->error_message;
        }

        if ($self->has_next_handler) {
            return $self->next_handler->validate($input, $errors);
        }

        return $errors;
    }

    sub _do_validate ($self, $value, $input) {
        die "サブクラスで_do_validateを実装してください";
    }
};

# ===== 具体的なバリデータ =====

# 必須チェック
package RequiredValidator {
    use Moo;
    extends 'Validator';

    sub _do_validate ($self, $value, $input) {
        return $value ne '';
    }
};

# メールアドレス形式チェック
package EmailValidator {
    use Moo;
    extends 'Validator';

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';
        return $value =~ /\A[^@\s]+\@[^@\s]+\.[^@\s]+\z/;
    }
};

# 文字列長チェック
package LengthValidator {
    use Moo;
    extends 'Validator';

    has min_length => (is => 'ro', default => 0);
    has max_length => (is => 'ro');

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';
        my $len = length($value);
        return 0 if $len < $self->min_length;
        return 0 if defined $self->max_length && $len > $self->max_length;
        return 1;
    }
};

# 正規表現チェック
package PatternValidator {
    use Moo;
    extends 'Validator';

    has pattern => (is => 'ro', required => 1);

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';
        return $value =~ $self->pattern;
    }
};

# 確認一致チェック
package ConfirmValidator {
    use Moo;
    extends 'Validator';

    has confirm_field => (is => 'ro', required => 1);

    sub _do_validate ($self, $value, $input) {
        return 1 if $value eq '';  # 空かどうかは RequiredValidator で見る
        my $original = $input->{$self->confirm_field} // '';
        return $value eq $original;
    }
};

# 真偽値チェック(利用規約同意など)
package BooleanValidator {
    use Moo;
    extends 'Validator';

    sub _do_validate ($self, $value, $input) {
        return $value ? 1 : 0;
    }
};

# 数値範囲チェック
package RangeValidator {
    use Moo;
    extends 'Validator';

    has min_value => (is => 'ro');
    has max_value => (is => 'ro');

    sub _do_validate ($self, $value, $input) {
        return 1 if !defined $value || $value eq '';
        return 0 unless $value =~ /\A[0-9]+\z/;
        return 0 if defined $self->min_value && $value < $self->min_value;
        return 0 if defined $self->max_value && $value > $self->max_value;
        return 1;
    }
};

# ===== チェーン構築 =====
package main;

sub build_user_registration_chain () {
    my $name_required = RequiredValidator->new(
        field_name    => 'name',
        error_message => '名前を入力してください',
    );

    my $email_required = RequiredValidator->new(
        field_name    => 'email',
        error_message => 'メールアドレスを入力してください',
    );

    my $email_format = EmailValidator->new(
        field_name    => 'email',
        error_message => 'メールアドレスの形式が正しくありません',
    );

    my $password_required = RequiredValidator->new(
        field_name    => 'password',
        error_message => 'パスワードを入力してください',
    );

    my $password_length = LengthValidator->new(
        field_name    => 'password',
        error_message => 'パスワードは8文字以上で入力してください',
        min_length    => 8,
    );

    my $password_upper = PatternValidator->new(
        field_name    => 'password',
        error_message => 'パスワードには大文字を含めてください',
        pattern       => qr/[A-Z]/,
    );

    my $password_lower = PatternValidator->new(
        field_name    => 'password',
        error_message => 'パスワードには小文字を含めてください',
        pattern       => qr/[a-z]/,
    );

    my $password_digit = PatternValidator->new(
        field_name    => 'password',
        error_message => 'パスワードには数字を含めてください',
        pattern       => qr/[0-9]/,
    );

    # パスワードにエラーがないときだけ、確認用を検証する
    my $confirm_required = RequiredValidator->new(
        field_name    => 'password_confirm',
        error_message => '確認用パスワードを入力してください',
        depends_on    => 'password',
    );

    my $password_confirm = ConfirmValidator->new(
        field_name    => 'password_confirm',
        error_message => 'パスワードが一致しません',
        confirm_field => 'password',
        depends_on    => 'password',
    );

    my $agreed_terms = BooleanValidator->new(
        field_name    => 'agreed_terms',
        error_message => '利用規約に同意してください',
    );

    my $age_required = RequiredValidator->new(
        field_name    => 'age',
        error_message => '年齢を入力してください',
    );

    my $age_number = PatternValidator->new(
        field_name    => 'age',
        error_message => '年齢は数値で入力してください',
        pattern       => qr/\A[0-9]+\z/,
    );

    my $age_range = RangeValidator->new(
        field_name    => 'age',
        error_message => '18歳以上の方のみ登録できます',
        min_value     => 18,
    );

    # チェーンを構築
    $name_required
        ->set_next($email_required)
        ->set_next($email_format)
        ->set_next($password_required)
        ->set_next($password_length)
        ->set_next($password_upper)
        ->set_next($password_lower)
        ->set_next($password_digit)
        ->set_next($confirm_required)
        ->set_next($password_confirm)
        ->set_next($agreed_terms)
        ->set_next($age_required)
        ->set_next($age_number)
        ->set_next($age_range);

    return $name_required;
}

sub validate_user ($input) {
    my $chain = build_user_registration_chain();
    my $errors = {};

    $chain->validate($input, $errors);

    if (%$errors) {
        return { ok => 0, errors => $errors };
    }

    return { ok => 1, data => $input };
}

# ===== 実行例 =====
my @test_cases = (
    # テスト1: すべて正常
    {
        name             => '山田太郎',
        email            => 'yamada@example.com',
        password         => 'Password123',
        password_confirm => 'Password123',
        agreed_terms     => 1,
        age              => 25,
    },
    # テスト2: 複数のエラー
    {
        name             => '',
        email            => 'invalid',
        password         => 'weak',
        password_confirm => 'mismatch',
        agreed_terms     => 0,
        age              => 15,
    },
    # テスト3: パスワードに大文字がない
    {
        name             => '鈴木花子',
        email            => 'suzuki@example.com',
        password         => 'password123',
        password_confirm => 'password123',
        agreed_terms     => 1,
        age              => 30,
    },
    # テスト4: 確認パスワード不一致
    {
        name             => '佐藤次郎',
        email            => 'sato@example.com',
        password         => 'Password123',
        password_confirm => 'Password456',
        agreed_terms     => 1,
        age              => 22,
    },
    # テスト5: 年齢が数値でない
    {
        name             => '田中三郎',
        email            => 'tanaka@example.com',
        password         => 'Password123',
        password_confirm => 'Password123',
        agreed_terms     => 1,
        age              => 'twenty',
    },
    # テスト6: 年齢が空
    {
        name             => '高橋四郎',
        email            => 'takahashi@example.com',
        password         => 'Password123',
        password_confirm => 'Password123',
        agreed_terms     => 1,
        age              => '',
    },
    # テスト7: 確認用パスワードが空
    {
        name             => '伊藤五郎',
        email            => 'ito@example.com',
        password         => 'Password123',
        password_confirm => '',
        agreed_terms     => 1,
        age              => 40,
    },
);

for my $i (0 .. $#test_cases) {
    say "=== テスト" . ($i + 1) . " ===";
    my $result = validate_user($test_cases[$i]);

    if ($result->{ok}) {
        say "検証成功: $result->{data}{name} ($result->{data}{email})";
    }
    else {
        say "検証失敗:";
        for my $field (sort keys $result->{errors}->%*) {
            say "  - $field: $result->{errors}{$field}";
        }
    }
    say "";
}

実行すると、次のように出力されます。テスト1〜5は、前回の完成コードと同じ結果です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
=== テスト1 ===
検証成功: 山田太郎 (yamada@example.com)

=== テスト2 ===
検証失敗:
  - age: 18歳以上の方のみ登録できます
  - agreed_terms: 利用規約に同意してください
  - email: メールアドレスの形式が正しくありません
  - name: 名前を入力してください
  - password: パスワードは8文字以上で入力してください

=== テスト3 ===
検証失敗:
  - password: パスワードには大文字を含めてください

=== テスト4 ===
検証失敗:
  - password_confirm: パスワードが一致しません

=== テスト5 ===
検証失敗:
  - age: 年齢は数値で入力してください

=== テスト6 ===
検証失敗:
  - age: 年齢を入力してください

=== テスト7 ===
検証失敗:
  - password_confirm: 確認用パスワードを入力してください
comments powered by Disqus
システム開発・AIワークフローのご相談は Meetsource へ
Hugo で構築されています。
テーマ Stack は Jimmy によって設計されています。