以下のコードはDigest::MD5のオブジェクトAPIの使用方法。
これはどういうことになっているかというと、最初のhexdigst()の呼び出しはそれまでにadd()で追加してきたデータを使用してMD5の計算をしているのだが、hexdigest()を呼んだ瞬間に 内部ステートがリセットされて、また一からデータを追加していくことになる。
ということは、最初のhexdigest()は "foobarbaz"から計算したMD5で、その次は"hoge", 最後のは空文字列から計算した値。
Digest::MD5は一旦MD5を計算したら、それまでのステートをそこから先に持ち越さない。
この実装は当然だと思う。なんでかというと このオブジェクトは引数から値を作成するためのオブジェクトであって、何かの状態を表すためのオブジェクトではない。計算をするためのオブジェクトは、その計算を確定するまでステートを保持してもいいけど、その後は持っている必要も意味もないし、それよりか有害だ。$md5->add("hoge")した時にそれまでの"foo", "bar", "baz"から計算するための途中結果とかが残ってて計算結果に影響を与えたら、とか想像するとgkbrじゃない?
これに対して、例えばDBから値をもってきてそこから何か思い計算をする何か・・・例えばあなたの株口座を表すオブジェクトがあって、それを使い回している。時折特定の銘柄の過去一ヶ月の自分の株の損益評価をグラフにしたいからそのシリーズを取得しなければいけないようなオブジェクトならどうか
これだと$codeの値が変わってもlast_30daysの値が変わらない。ひどい罠ですね。以下のように必ずどの銘柄コードのキャッシュなのかわかるように格納しないとまざってしまう危険性が高い。
あと大事なのはキャッシュをするなら、キャッシュがパージされるタイミングがあるのかよく考えること。この例の場合は日付が変わったら当然新しい値が追加されて、一番古い値が消えるだろうから キャッシュパージのタイミングは日付だ。だからキャッシュ内にいつまで有効なデータか書いておくか、memcachedのような「別に消えても大丈夫」なキャッシュだったらキーに日付も含めておけばいい。
上記のようなステート保持が必要ないけど、それでも重い処理ならまずは「変数」として持って、必要な関数に渡しましょう。一番シンプルです!
どうしてもオブジェクトに保存したいならlocalを使うか、ガードオブジェクトを使いましょう。
最後に、それでもどうしてもそういうコードになってしまう場合。そういう事もあるかとは思います。現実はセオリー通りにはいかないです。その場合はしょうがないのででっかいコメントブロックを儲けて、なんでそんな事をしないといけなかったか、400文字程度の反省文を該当メソッドの前に書いてください。 そこまですれば後任の人が髪の毛をむしる必要がなくなりますね。
よろしくお願いいたします。
use strict;最初のhexdigest() の呼び出しは "foo", "bar", "baz"の値を使って計算したMD5の値。次の呼び出しは違う値を作成し、その次もまた値は違う。
use Digest::MD5;
my $md5 = Digest::MD5->new;
$md5->add("foo");
$md5->add("bar");
$md5->add("baz");
warn $md5->hexdigest; # 6df23dc03f9b54cc38a0fc1483df6e21
$md5->add("hoge");
warn $md5->hexdigest; # ea703e7aa1efda0064eaa507d9e8ab7e
warn $md5->hexdigest; # d41d8cd98f00b204e9800998ecf8427e
これはどういうことになっているかというと、最初のhexdigst()の呼び出しはそれまでにadd()で追加してきたデータを使用してMD5の計算をしているのだが、hexdigest()を呼んだ瞬間に 内部ステートがリセットされて、また一からデータを追加していくことになる。
ということは、最初のhexdigest()は "foobarbaz"から計算したMD5で、その次は"hoge", 最後のは空文字列から計算した値。
Digest::MD5は一旦MD5を計算したら、それまでのステートをそこから先に持ち越さない。
この実装は当然だと思う。なんでかというと このオブジェクトは引数から値を作成するためのオブジェクトであって、何かの状態を表すためのオブジェクトではない。計算をするためのオブジェクトは、その計算を確定するまでステートを保持してもいいけど、その後は持っている必要も意味もないし、それよりか有害だ。$md5->add("hoge")した時にそれまでの"foo", "bar", "baz"から計算するための途中結果とかが残ってて計算結果に影響を与えたら、とか想像するとgkbrじゃない?
これに対して、例えばDBから値をもってきてそこから何か思い計算をする何か・・・例えばあなたの株口座を表すオブジェクトがあって、それを使い回している。時折特定の銘柄の過去一ヶ月の自分の株の損益評価をグラフにしたいからそのシリーズを取得しなければいけないようなオブジェクトならどうか
my $portfolio = Stock::Portfolio->new( user_id => "lestrrat ");このような計算を必要とするなら過去30日分の株価をオブジェクト内にキャッシュしてるのはわかる。ただしここでも気をつけないといけないのはキャッシュするにしても銘柄コードをキーにしてキャッシュしないといけない。例えばインメモリでキャッシュするにしても以下のように"last_30days"のようなキーでキャッシュしてはいけない
while ( ... ) {
# 時折 NTTの過去一ヶ月の取得株価と任意の日の株価の差額をリストで欲しい
my @profit = $portfolio->get_profits_last_30days( 9432 );
# そのほか $portfolioで色々操作するから $portfolio自体は
# 使い回す。
}
sub get_profits_last_30days {
my ($self, $code) = @_;
...
$self->{last_30days} ||= $self->get_prices_last_30days( $code );
...
}
これだと$codeの値が変わってもlast_30daysの値が変わらない。ひどい罠ですね。以下のように必ずどの銘柄コードのキャッシュなのかわかるように格納しないとまざってしまう危険性が高い。
sub get_profits_last_30days {
my ($self, $code) = @_;
...
$self->{last_30days}->{$code} ||= $self->get_prices_last_30days( $code );
...
}
あと大事なのはキャッシュをするなら、キャッシュがパージされるタイミングがあるのかよく考えること。この例の場合は日付が変わったら当然新しい値が追加されて、一番古い値が消えるだろうから キャッシュパージのタイミングは日付だ。だからキャッシュ内にいつまで有効なデータか書いておくか、memcachedのような「別に消えても大丈夫」なキャッシュだったらキーに日付も含めておけばいい。
上記のようなステート保持が必要ないけど、それでも重い処理ならまずは「変数」として持って、必要な関数に渡しましょう。一番シンプルです!
どうしてもオブジェクトに保存したいならlocalを使うか、ガードオブジェクトを使いましょう。
# 最もシンプル。変数に格納しておくあともう一つの方法としては関数開始時に格納してあるフィールドを初期化する、とかあるけど、そもそもただのキャッシュで、次に計算する値が同じ物になる確率が低いなら関数を抜けた時にちゃんとクリーンアップされてるほうが筋がいいと思う。
sub foo {
my $self = shift;
my $data = $self->calculate_some_data; # 重い処理
$self->bar( $data );
$self->baz( $data );
}
# どうしても $selfに入れたい (1) local
sub foo {
my $self = shift;
local $self->{data} = $self->calculate_some_data; # 重い処理
$self->bar; # $self->get_data(); とかでキャッシュされたデータにアクセス
$self->baz;
}
# どうしても$selfに入れたい (2) ガードオブジェクト
use Scope::Guard;
sub foo {
my $self = shift;
my $guard = Scope::Guard->new(sub {
delete $self->{data};
});
$self->{data} = $self->calculate_some_data; # 重い処理
$self->bar;
$self->baz;
}
最後に、それでもどうしてもそういうコードになってしまう場合。そういう事もあるかとは思います。現実はセオリー通りにはいかないです。その場合はしょうがないのででっかいコメントブロックを儲けて、なんでそんな事をしないといけなかったか、400文字程度の反省文を該当メソッドの前に書いてください。 そこまですれば後任の人が髪の毛をむしる必要がなくなりますね。
よろしくお願いいたします。
コメント