難しいコードを分解する話
yaboooブログのパスワードわからなくなったので一時的にこっちに書く。
たとえばarrayリファレンスをいろいろな文字列フォーマットに変換する話を徐々にわかりやすく解体する。
複雑なメソッド
ひとつのメソッドで
StringLister->new->output( 'wiki' => [qw/hello world/]);
みたいに全部の情報を載せて書いて、フォーマットが複雑になってしまった場合に
こんなメソッドが出来上がる。
{ package StringLister; use strict; use warnings; sub new{ my ($class) = @_; return bless {} =>$class; } sub output{ my ($self,$format,$target) = @_; my $ret = ''; if( $format eq 'wiki' ){ $ret.= "\n"; }elsif($format eq 'html'){ $ret.= "<ul>\n"; }elsif($format eq 'tex'){ $ret.= "\\begin{itemize}\n"; } foreach my $item( @{$target} ){ if( $format eq 'wiki' ){ $ret.= "- $item\n"; }elsif($format eq 'html'){ $ret.= "<li>$item</li>\n"; }elsif($format eq 'tex'){ $ret.="\\item $item\n"; } } if( $format eq 'wiki' ){ $ret.= ""; }elsif($format eq 'html'){ $ret.= "</ul>\n"; }elsif($format eq 'tex'){ $ret.= "\\end{itemize}\n"; } return $ret; } } 1;
最初はhtmlだけだったのにどんどんフォーマットが増えてしまった場合とか
そんな風になりそうな感じというやつ。
とりあえずこれでも単純だと思う奴はもっと複雑なのを想像して。
クラスを作成して保守できるように
しかたないのでクラスを作る。
outputという複雑なメソッドをComposed Method的に分解してみる。
StringLister->new( 'wiki' => [qw/hello world/])->output
みたいに書きたい場合。
{ package StringLister; use strict; use warnings; sub new{ my ($class,$format,$list) = @_; return bless { format => $format, list => $list, } =>$class; } sub format{shift->{format}} sub list{@{shift->{list}}} sub header { my $self = shift; if( $self->format eq 'wiki' ){ return "\n"; }elsif($self->format eq 'html'){ return "<ul>\n"; }elsif($self->format eq 'tex'){ return "\\begin{itemize}\n"; } die; } sub footer { my $self = shift; if( $self->format eq 'wiki' ){ return ""; }elsif($self->format eq 'html'){ return "</ul>\n"; }elsif($self->format eq 'tex'){ return "\\end{itemize}\n"; } } sub each_item { my $self = shift; my $item = shift; if( $self->format eq 'wiki' ){ return "- $item\n"; }elsif($self->format eq 'html'){ return "<li>$item</li>\n"; }elsif($self->format eq 'tex'){ return "\\item $item\n"; } } sub output{ my $self = shift; return sprintf("%s%s%s", $self->header, (join '' ,map { $self->each_item($_) } $self->list), $self->footer ); } } 1;
これでもまだIF文とかがそこかしこにあってダサい。
もっとOOPっぽく、かつこれからStringListerにはformatが続々と増える予定らしいので
分解を考える。
TemplateMethodパターンを使った場合
テンプレートメソッドパターンっていうのは、比較的単純なデザインパターンの一種で、
親クラス側にはやりたい枠組みだけ書いて、子供の方で具体的な仕事するメソッドを実装するという方法。
今回はFactoryも使って継承構造が外に出ないようにしてみる。
{ package AbstractStringLister; use strict; use warnings; sub new { my ( $class, $list ) = @_; return bless { list => $list } ,$class; } sub header { die 'abstract'; } sub footer { die 'abstract'; } sub each_item { die 'abstract'; } sub list { return @{ +shift->{list} }; } sub output { my $self = shift; return sprintf("%s%s%s", $self->header, (join '' ,map { $self->each_item($_) } $self->list), $self->footer ); } 1; }
こんな感じに抽象的な振る舞い部分だけ書いておいて、それ単体では使わずに
継承した子クラスに意味を持たせる。
{ package HTMLStringLister; use strict; use warnings; push our @ISA, 'AbstractStringLister'; sub header{ return "<ul>\n"; } sub footer{ return "</ul>\n" } sub each_item{ my ($self,$item) = @_; return sprintf("<li>%s</li>\n",$item); } } { package WikiStringLister; use strict; use warnings; push our @ISA, 'AbstractStringLister'; sub header{ return "\n"; } sub footer{ return ""; } sub each_item{ my ($self,$item) = @_; return sprintf("- %s\n",$item); } } { package TexStringLister; use strict; use warnings; push our @ISA, 'AbstractStringLister'; sub header{ return "\\begin{itemize}\n"; } sub footer{ return "\\end{itemize}\n" } sub each_item{ my ($self,$item) = @_; return sprintf("\\item %s\n",$item); } }
フォーマットごとに子供できたので、それに仕事書けばいいと。
さらに外から使いやすいようにファクトリを作っておく。
{ package StringListerFactory; use strict; use warnings; use constant FORMAT_TO_LISTER =>{ 'wiki' => q|WikiStringLister|, 'tex' => q|TexStringLister| , 'html' => q|HTMLStringLister|, }; sub create{ my ($class,$format,$list ) = @_; return unless FORMAT_TO_LISTER->{$format}; return FORMAT_TO_LISTER->{$format}->new( $list ); } } 1;
こんなんで
StringListerFactory->create('wiki' => [qw/hello world/] )->output;
見たいにかけるようになった。
委譲をつかって、ストラテジーパターン的に解決した場合
継承最高だぜ!差分プログラミングだぜ!といっていたものの
逆にTemplate Methodだけだと、
関数ごとにいろいろな差し替えをするのが難しくなってしまう。
StringListerの場合、Formatという抽象がほしくなってきた場合に
親子関係は癒着が強すぎて離すことが難しかったりする。
そこで、委譲で書き換え。
{ package StringLister; use strict; use warnings; sub new { my ( $class, $formatter ,$list) = @_; return bless { list => $list ,formatter => $formatter} ,$class; } sub formatter{shift->{formatter}} sub header { shift->formatter->list_header; } sub footer { shift->formatter->list_footer; } sub each_item { my ($self,$item) = @_; $self->formatter->list_each_item( $item ); } sub list { return @{ +shift->{list} }; } sub output { my $self = shift; return sprintf("%s%s%s", $self->header, (join '' ,map { $self->each_item($_) } $self->list), $self->footer ); } 1; }
フォーマット関連の処理部分を外側からもらって、それを利用するように書き換える。
{ package HTMLFormatter; use strict; use warnings; sub new { bless {}, shift } sub list_header{ return "<ul>\n"; } sub list_footer{ return "</ul>\n" } sub list_each_item{ my ($self,$item) = @_; return sprintf("<li>%s</li>\n",$item); } } { package WikiFormatter; use strict; use warnings; sub new { bless {}, shift } sub list_header{ return "\n"; } sub list_footer{ return ""; } sub list_each_item{ my ($self,$item) = @_; return sprintf("- %s\n",$item); } } { package TexFormatter; use strict; use warnings; sub new { bless {}, shift } sub list_header{ return "\\begin{itemize}\n"; } sub list_footer{ return "\\end{itemize}\n" } sub list_each_item{ my ($self,$item) = @_; return sprintf("\\item %s\n",$item); } } 1;
こんな感じにするとそれぞれのフォーマットごとの分離ができた。
StringLister->new( TexFormatter->new => [qw/hello world/] )->output
見たいに書ける。