Hatena::ブログ(Diary)

Islands in the byte stream

2010-11-17

Re: バリデーションはどの位置で必要か

バリデーションはどの位置で必要か - サンプルコードによるPerl入門

バリデーションはアクセサメソッドの内部で行うのではなく、バリデーションの専用のモジュールを使用して、データを受け取った入り口で行うのがよいでしょう。

私はこれには反対です。アプリケーションにせよライブラリにせよ、原則としてすべての公開APIの入力値はバリデーションするのが望ましいと考えます。

すなわち、アクセサメソッドもそれがパブリックなAPIであればバリデーションをしたほうがいいと思います。

とはいえ以下の基本的な考え方に異論があるわけではありません。

データのバリデーションを行う主要な目的は、外部から入力されるデータが正しい値かどうかをチェックするためのものです。

問題は、どこまでを「外部からの入力」とみなすかということでしょう。私は、そのプログラム/ライブラリのパブリックなAPIが受け取る値はすべて「外部からの入力」であると考えます。したがって、ライブラリのユーザーが呼び出すメソッド/アクセサの値は原則としてバリデーションすべきです。もちろんユーザーがそのライブラリに慣れるにしたがってバリデーションの価値は下がってゆき、最終的には(そのユーザーにとって)無用の長物になります。したがって、実際にバリデーションを行うかどうかはパフォーマンスとのトレードオフになり、結果としてアクセサ等ではバリデーションを行わないことが多いという現状はあるでしょう。

個人的には、引数のバリデーションは言語機能として存在するのが望ましく、間違った値を渡そうとするとコンパイルすら通らないというのが理想だと思っています。

ところでid:perlcodesampleさんはバリデーションや値の変換を行うとデバッグがしにくいと書いていますが、これには違うでしょう。むしろ、バリデーションはデバッグをしやすくするための仕組みです。また値の変換(coercion)は原則として自然な範囲に収めるべきで、驚くような変換はそもそもすべきではありません。これはたとえば、Data::Validatorの出力するエラーメッセージを見ればお分かりいただけるかと思います。

#!perl -w
use 5.10.0;
use strict;
use Data::Validator;

sub add0 {
    my $args = {@_};
    return $args->{x} + $args->{y};
}

sub add1 {
    state $rule = Data::Validator->new(
        x => 'Int',
        y => 'Int',
    );
    my $args = $rule->validate(@_);
    return $args->{x} + $args->{y};
}

say "add(x => 1, y => 'foo')";
say '# 警告とともに誤った値を返す';
say eval { add0(x => 1, y => 'foo') } || $@;
say '# yの値がおかしいと言って死ぬ';
say eval { add1(x => 1, y => 'foo') } || $@;

say "add(x => 1)";
say '# 警告とともに誤った値を返す';
say eval { add0(x => 1) } || $@;
say '# yが渡されていないといって死ぬ';
say eval { add1(x => 1) } || $@; 

say "add(x => 1, y => 2, z => 3)";
say '# zを単に無視する';
say eval { add0(x => 1, y => 2, z => 3) } || $@;
say '# zなんて知らないといって死ぬ';
say eval { add1(x => 1, y => 2, z => 3) } || $@;
__END__

Result:

$ perl errors.pl
add(x => 1, y => 'foo')
# 警告とともに誤った値を返す
Argument "foo" isn't numeric in addition (+) at - line 8.
1
# yの値がおかしいと言って死ぬ
Invalid value for 'y': Validation failed for 'Int' with value foo
... found at lib/Data/Validator.pm line 215
	Data::Validator::validate('Data::Validator=HASH(0x8a2ba08)', 'x', 1, 'y', 'foo') called at - line 16
	main::add1('x', 1, 'y', 'foo') called at - line 24
	eval {...} called at - line 24

add(x => 1)
# 警告とともに誤った値を返す
Use of uninitialized value in addition (+) at - line 8.
1
# yが渡されていないといって死ぬ
Missing parameter: 'y'
... found at lib/Data/Validator.pm line 215
	Data::Validator::validate('Data::Validator=HASH(0x8a2ba08)', 'x', 1) called at - line 16
	main::add1('x', 1) called at - line 30
	eval {...} called at - line 30

add(x => 1, y => 2, z => 3)
# zを単に無視する
3
# zなんて知らないといって死ぬ
Unknown parameter: 'z'
... found at lib/Data/Validator.pm line 215
	Data::Validator::validate('Data::Validator=HASH(0x8a2ba08)', 'x', 1, 'y', 2, 'z', 3) called at - line 16
	main::add1('x', 1, 'y', 2, 'z', 3) called at - line 36
	eval {...} called at - line 36

警告をだしてくれるのはまだいい方で、間違った値を黙ってそのまま使うというのが一番やっかいです。それくらいなら、初めからはっきりと「値が間違っている」といって例外を吐いてくれた方がデバッグしやすいのではないでしょうか*1

*1:もっとも最後の例、未知のパラメータに対して例外を放出するのは厳しすぎるという考えもあり、オプションで無効にできるようになっています。どのくらい厳しくすべきかは好みの問題だと思います。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証