Mooseクラスの生成時に、コンストラクター引数として与えられたハッシュまたはハッシュリファレンスに着目して処理を行いたい場合があります。
例えば、初期化ハッシュキーを跨いだ検証などが挙げられます。triggerの使い方についての記事で説明用に作った、元号と西暦のアトリビュートを持つ昭和クラスで想定してみます。
元号と西暦の両方を指定された場合、その両方のアトリビュートが未指定になります。undefという値が入っているのではなく、has_imperial_eraなどのpredicateメソッドの戻り値が偽であるという意味です。
それでは困るので、コンストラクター引数に指定するのは元号と西暦のどちらか一方のみに制限する......という要件が出て来たこととします。その場合、ハッシュとハッシュリファレンスの両方でキーを見付ける処理をBUILDARGSに書くのではなく、親クラス(最終的な親はMoose::Object)のBUILDARGSを呼ぶようにすると、素直に書けます。
# ...
around BUILDARGS => sub {
my $next = shift;
my $class = shift;
# ここでは@_はハッシュかも知れないしハッシュリファレンスかも知れない
# 親クラス(Moose::Object)のハッシュリファレンス化はBUILDARGSに任せる
my $init_args = $class->$next(@_); # ここではハッシュリファレンス
confess 'Initialization argument must be '
. 'any one of imperial_era or christian_era'
if exists $init_args->{imperial_era}
&& exists $init_args->{christian_era};
return $init_args;
};
# ...
こんなものは常識なのでしょうけれども、意外とこういう初歩的なところの認識が甘くて、今までは(後述するように)ハッシュリファレンスのみを食べるような偏屈APIを撒き散らしていました。反省反省。
以下は補足情報です。
メソッドモディファイヤーを使わない場合
親クラス(Moose::Object)のBUILDARGSを修飾せず、自クラスでBUILDARGSクラスメソッドを定義しても、親クラスのBUILDARGSを呼べば同じ事です。
# ...
sub BUILDARGS {
my $class = shift;
my $init_args = $class->SUPER::BUILDARGS(@_);
confess 'Initialization argument must be '
. 'any one of imperial_era or christian_era'
if exists $init_args->{imperial_era}
&& exists $init_args->{christian_era};
return $init_args;
}
# ...
親のBUILDARGSを使わない場合
Mooseのコンストラクターは、デフォルトではハッシュリファレンスかハッシュを受け付けます。自分のBUILDARGSクラスメソッド内で、コンストラクター引数がハッシュリファレンスであるかハッシュであるかを判断する処理を書くのは、とても面倒です。
# ...
sub BUILDARGS {
my $class = shift;
# @_がハッシュであるかハッシュリファレンスであるかを判別する処理は、
# Moose::Object::BUILDARGSと同じことなので面倒
# ...
}
# ...
自分のクラスではコンストラクター引数にハッシュリファレンスのみを受け付ける(ハッシュは受け付けない)と決めてしまえば、例えば以下のようにも書けるでしょう。
# ...
sub BUILDARGS {
my $class = shift;
if (@_ == 1 && ref $_[0] eq 'HASH') {
confess 'Initialization argument must be '
. 'any one of imperial_era or christian_era'
if exists $_[0]->{imperial_era}
&& exists $_[0]->{christian_era};
return $_[0]; # もしくは$class->SUPER::BUILDARGS($_[0]);
}
else {
return $class->SUPER::BUILDARGS(@_);
}
}
# ...
ただ、ハッシュを敢えて拒否するような理由がない限りは、費やすべき労力はそれほど変わらないので、素直にMoose::Object::BUILDARGSを呼んだ方が良いと考えます。
余談: ハッシュやハッシュリファレンス以外の引数を使いたい場合
Moose::Manual::Constructionでは、それ以外の場合(例えばスカラー値など)の対処法が書かれています。今回の要件とは違いますが、後輩への説明用に余談として述べておきます。
Personクラスは、米国在住者の背番号(つまりは一意に識別出来る、id)である社会保障番号(ssn : Social Security Number)の文字列を渡すだけでオブジェクトを生成してくれると便利だ、という例です。
# ...
has 'ssn' => ( # 社会保障番号
is => 'rw',
);
# ...
around BUILDARGS => sub {
my $next = shift;
my $class = shift;
if (@_ == 1 && ! ref $_[0]) {
return $class->$next(ssn => $_[0]);
}
else {
return $class->$next(@_);
}
};
# ...
昔のバージョンのMoose::Manual::Constructionでは、メソッドモディファイヤーで親を修飾するのではなく、自クラスでBUILDARGSクラスメソッドを実装するように書かれていましたが、これも要は同じ事です。
# ...
sub BUILDARGS {
my $class = shift;
if (@_ == 1 && ! ref $_[0]) {
return {
ssn => $_[0],
};
# 必要に応じて、以下のようにする方が良いかも知れません
# $class->SUPER::BUILDARGS({ssn => $_[0]});
}
else {
return $class->SUPER::BUILDARGS(@_);
}
}
# ...
余談: BUILDかBUILDARGSか
BUILDの引数は、コンストラクター引数にハッシュを渡そうがハッシュリファレンスを渡そうが、一律にハッシュリファレンスとなっています。従って、技術的には以下のようにBUILDでバリデーションロジックを設けることも不可能ではありません。
# ...
sub BUILD {
my $self = shift;
confess 'Initialization argument must be '
. 'any one of imperial_era or christian_era'
if exists $_[0]->{imperial_era}
&& exists $_[0]->{christian_era};
return;
}
# ...
しかし、既にオブジェクトを作ってしまった後に、そのオブジェクトの初期化変数を検証するのは、順序が逆転しています。おかしな値を拒絶するのは出来るだけ早い段階の方が良いので、今回の場合はBUILDARGSでフックしています。
勿論、生成されたオブジェクトのアトリビュートを跨いだ検証などは、素直にBUILDを使えば良いです。
この辺りは、初期化時に行いたい処理によって、BUILDARGSにフックするのかBUILDにフックするのかを決めれば良いでしょう。迷ったら、早めに死ぬようにBUILDARGSを選ぶのが無難です(そして、きちんと検証出来ていることをテストで確かめましょう)。
コメントする