Class::Delegateが便利だったのでメモ。
PerlのOOは必ずしもよくできてるとは言い難いけど、「使える範囲で十分に」書ける。これは決して悪いことじゃないし、Perlの良いところであり僕も好きな点なんだけど、コンストラクタについてはピンと来ない場合がある。
社内のフレームワークでHTML::Templateを継承していたクラスがあった。そのコンストラクタ内が以下のようになっていた。*1
package HogeHoge; use base q/HTML::Template/; sub new { my ($class, $filename) = @_; $class = ref($class) if ( ref($class) ); # HTML::Templateのタグを変更. # 詳細は perldoc HTML::Templateでfilterを検索. my $filter = sub { # フィルタ関数 # ... }; # $pathを決定する # ... # HTML::Template生成. my $self ={}; eval { $self = $class->SUPER::new(path => $path, filename => $filename, die_on_bad_params => 0, filter => $filter, loop_context_vars => 1); }; if($@) { # エラー処理 # ... } return bless $self, $class; }
要は、
use HogeHoge; my $template = HogeHoge->new('somepage.html'); $template->param(price => '19,800'); $template->output;
するのを想定してる設計。
それまではそれなりに動いていたんだけど、これだと気持ちが悪い。なぜなら、
$self = $class->SUPER::new(path => $path, filename => $filename, die_on_bad_params => 0, filter => $filter, loop_context_vars => 1);
して返ってくるオブジェクトは、HTML::Templateの内部を知らない限りハッシュリファレンスかどうかは分からないので、HogeHogeクラスで新しいフィールドを追加しようとして
$self->{logger} = $logger;
などとできない。もちろんやって動くかもしれないけど、そもそもHTML::Templateクラスはそんな使われ方は想定していないから、不慮の現象が起こらないとも限らないだろう。
そもそもHogeHogeクラスの設計として、HTML::Templateを継承するというのは不適切で、Has-aとするのが適切なんじゃないかしら?と思ったんだけど、HTML::Templateを継承しないで、HogeHogeクラスのフィールドとしてそのインスタンスを持つようにすると、既存のコードでHogeHogeクラスがHTML::Templateのサブクラスとして呼び出しているメソッド(param()とか)を逐一HogeHogeクラスで実装して、対応するHTML::Templateインスタンスにパスして透過的にアクセスできるようにしてやらなくちゃならない。また、現状呼び出されるメソッドが限定されていればよいけど、今後HogeHogeクラスを通してHTML::Templateの異なるメソッドを使用したかったら、逐一実装する必要がある*2。これはイケてない。
どちらかといえば委譲がすんなり行くのでは、ということでCPANを彷徨い歩いたら、ありました。
Class::Delegateモジュール。
Class::Delegate - easy-to-use implementation of object delegation.
Class::Delegateのpodより
詳細はpodに譲るとして、以下のような使い方ができるらしい。
#!/usr/bin/perl # Class::Delegateのテスト package My::Base; sub new { my $class = shift; $class = ref($class) if ( ref($class) ); return bless {}, $class; } sub a { print "My::Base#a called.\n"; } sub b { print "My::Base#b called.\n"; } package My::Delegated; use base qw( Class::Delegate ); # My::Baseではなく、Class::Delegateを継承 sub new { my $class = shift; $class = ref($class) if ( ref($class) ); my $base = new My::Base; my $self = { base => $base }; bless $self, $class; $self->add_delegate($base); # My::Delegatedにないメソッドは$baseに委譲する return $self; } sub a { my $self = shift; my $base = $self->{base}; print "My::Delegated#a called. \n"; } # sub b is not defined. package main; my $d = new My::Delegated; $d->a; # My::Delegated#aが呼ばれる $d->b; # $dがDelegateしているMy::Base#bが呼ばれる
そして出力:
My::Delegated#a called. My::Base#b called.
メソッドaはMy::Delegateクラスで有しているので、そのまま呼び出されるけど、メソッドbはMy::Delegateに実装がないので、委譲しているMy::Baseのインスタンスのメソッドが呼ばれる。
という訳で、HogeHogeは以下のように書き換え。
package HogeHoge; use HTML::Template; use base qw( Class::Delegate ); sub new { my ($class, $filename) = @_; # HTML::Templateのタグを変更. # 詳細は perldoc HTML::Templateでfilterを検索. my $filter = sub { # フィルタ関数 # ... }; # $pathを決定する # ... # HTML::Template生成. my $template; eval { $template = HTML::Template->new(path => $path, filename => $filename, die_on_bad_params => 0, filter => $filter, loop_context_vars => 1); } if($@) { # エラー処理 # ... } my $self = { template => $template }; bless $self => $class; # HTML::Templateにデリゲートする # HTML::Templateのextendとはしていないことに注意:-) $self->add_delegate($template); return $self; }
これでスッキリ!HogeHogeはハッシュリファレンスなのが分かっているので、HogeHogeのサブクラスを書くことだってすんなり。
CPANに乾杯!
類似のだとClass::Proxyてのもあるみたいだけど今回の状況はこっちの方がすんなり行くね。