難しいコードを分解する話

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

見たいに書ける。