― Web Technology and Life ―

Perlで継承と同時に親クラスにエクスポートされている関数を子クラスで使えるようにしたい

2011-12-04
Smart::Argsのargsとか親クラスにエクスポートで委譲されいる関数を子クラスでもuseせずに使えるようにしたいと思って、いろいろ試行錯誤した備忘録です。この記事はHachioji.pm#11での発表(ほとんど発表していないも同然でしたがw)を修正した内容をもとにしています。ついてに、Exporter::Baseってモジュールも作ってみました。

問題

明らかに子クラスとしての使用が限定されている場合、いちいち親クラスでuseしているもろもろを子クラスでuseするのがだるいと思います。例えば、こんな感じ。

#親クラス
package Calc;
use stirct;
use warnings;
use Smart::Args;

sub new {bless +{ _result => '' }, +shift }

sub input {
    args_pos my $self,
            my $inp_a => 'Int',
            my $inp_b => 'Int';

    $self->calc($inp_a,$inp_b);
}

sub calc { die 'you must be override this mothod' }

sub output { $_[0]->{_result} }

#子クラスA
package Calc::Addition;
use stirct;
use warnings;
use Smart::Args;
use parent 'Calc';

sub calc {
    args_pos my $self,
            my $inp_a => 'Int',
            my $inp_b => 'Int';

    $self->{_result} = $inp_a + $inp_b;
}

#子クラスB
package Calc::Subtraction;
use stirct;
use warnings;
use Smart::Args;
use parent 'Calc';

sub calc {
    args_pos my $self,
            my $inp_a => 'Int',
            my $inp_b => 'Int';

    $self->{_result} = $inp_a - $inp_b;
}

もう子クラスでは「use ~」とメインのコードが同じくらいになってしまっているだから、KISSの法則からいってもNGだと思うわけです。

要件

子クラスで親クラスでuseしているモジュールをいちいち「use ~」って書くのがだるいというのが理由なので、要件としては「子クラスでは親クラスをuse (parent) ~するだけ」です。基本的に、親クラスにハックをかけて実装するのが基本方針となります。

3つのアイデア

親モジュールに施すハックに、3つほどのアイデアが浮かびました。基本的には、継承の派生系なので、use parent we/Class/が前提です。はじめにまとめてしまうと、以下のようになります。

手法 子クラスですること 親クラスですること 結論
parentで貫く use parent qw/Class/; BEGINにゴニョゴニョ できなかった
Module::Pluggableを利用 use parent qw/Class/;子クラスは親クラスの名前空間以下 BEGINにゴニョゴニョ 堅実な実装
useの引数で「-base」 use Class qw/-base/; importにゴニョゴニョ 一番いい実装

まぁ二番目の「Module::Pluggableを利用」に関してはparentではなくてもいいのですが、 それでは、それぞれの手法を個別に解説します。

parentで貫く

この手法は、最初に思いついたアイデアでもあり、もっともシンプルなので、最後までこの手法に固執したのですが、どうもわかりませんでした。

既存研究

punitanさんが同じ試みをされたようでgfxさんがそれに答えています。gfxさんはparentをハックするという手法を紹介していますが、既存モジュールのハックはgfxさんクラスにPerlのインターナルを理解していないとどこまでリスキーかさえわからないので、Casualユーザーである私の範疇外でしょう。

試してみた方法

この手法の場合、parentをuseしたときになんらかのハックをかけるように実装しなくてはいけないので、parentのソースを確認します。すると、実際に親クラスをロードするのにrequireを使用しているので、importに何かを書き込むのは難しいことになります。よって、BEGIN内でゴニョゴニョすることになります。toku_bassさんに@INCからdeleteしてみたりとアイデアも頂きましたが、どうもうまくいきませんでした。具体的に、途中まで試してみたのが、以下です。

#テストコード
use strict;
use warnings;
use Test::More;
use Calc::Subtraction;
use Calc::Addition;

subtest 'Subtraction' => sub {
    my $c = Calc::Subtraction->new;

    $c->input(5,4);

    is $c->output,1;

};

subtest 'Addition' => sub {
    my $c = Calc::Addition->new;

    $c->input(5,4);

    is $c->output,9;

};

done_testing;

#親クラス
package Calc;
use strict;
use warnings;
use Smart::Args;

sub BEGIN {
    my $pkg = caller(3);

    return if($pkg eq 'main');

    for my $func ( qw/args args_pos/ ) {
        no strict 'refs';
        *{"$pkg\::$func"} = \&$func;
    }

    delete $INC{'Calc.pm'};
}

sub new {bless +{ _result => '' }, +shift }

sub input {
    args_pos my $self,
            my $inp_a => 'Int',
            my $inp_b => 'Int';


    $self->calc($inp_a,$inp_b);
}

sub calc {
    die 'you must be override this mothod';
}

sub output { $_[0]->{_result} }
1;

#子クラスA
package Calc::Addition;
use strict;
use warnings;
use parent 'Calc';

sub calc {
    args_pos my $self,
            my $inp_a => 'Int',
            my $inp_b => 'Int';

    $self->{_result} = $inp_a + $inp_b;
}
1;

#子クラスB
package Calc::Subtraction;
use strict;
use warnings;
use parent 'Calc';

sub calc {
    args_pos my $self,
            my $inp_a => 'Int',
            my $inp_b => 'Int';

    $self->{_result} = $inp_a - $inp_b;
}
1;

この手法は、mainスクリプトなどで、親クラスがロードされる前提でしかも、そのロードが他の子クラスよりも後というのが前提になるので、非常に微妙です。しかも、このテストコードではcaller(5)とかでうまく動いていますが、他の環境だと微妙な気がします。B::とかの何かを使えばできそうな気がしますが、ちょっと挫折ですね・・・。requireってrequireのソースみると、useと違って何回も呼ばれるって聞いていたけど、何回も呼ばれるは呼ばれるけどクラスをロードするのは最初の一回だけなんですね。まぁ、ここをハックするのも良からぬことが起きそうだからやめたほうがよさそう。あと、hide_o_55さんが気になるツイートしていたんだけど、B::Hooks::OP::Check::StashChangeとかパッと見で意味不明もいいところだったので、、、もう少しレベルアップしてからかなと(苦笑)

Module::Pluggableを利用

Module::Pluggableを利用すると、特定の名前空間以下にあるクラスを一括で呼び出すことができるので、親クラスのBEGINで自分の名前空間以下にあるクラスを一括で呼び出し、親モジュールがエクスポートでうけている関数を一括で当て込んでいく、という手法が考えられます。

既存研究

tokuhiromさんの、Amon2-3.28、Largeフレイバー、Dispather実装と、「Exporter::Auto ってのつくってみたよ」っていう話の解説記事コードがとても参考になりますね。

試してみた方法

こんな感じに実装できます。

#親クラス
package Calc;
use strict;
use warnings;
use Smart::Args;
use Module::Pluggable::Object;
use Sub::Identify;

sub BEGIN {
    my $parent = __PACKAGE__;

    my @children = Module::Pluggable::Object->new(
        search_path => [$parent],
    )->plugins;

    no strict 'refs';
    for my $child (@children) {
        for my $func (sort keys %{"${parent}::"}) {(#sort keys %{"${parent}::")を('args','args_pos')とかすればエクスポートしたい関数だけエクスポートできる
            next if $func =~ /^(?:BEGIN|CHECK|END)$/;
            next if $func =~ /^_/;
            my $code = *{"${parent}::${func}"}{CODE};
            next unless $code;
            next if $parent eq Sub::Identify::stash_name($code);

            *{"$child\::$func"} = $code;
        }
    }
}

#以下同上

名前空間上、子クラスは親クラスの下くるべきだし、これで十分な気がしますが、ふとこれを書いて思ったのが、「子クラスで『use parent qw/Class/』ってしているだけなのに、勝手に親クラスにエクスポートされている関数も使えるようになっているのってわかりづらくない?」、ということでした。はじめは、parentに固執していたんですが、実はむしろそれは微妙というふうに思ったのです。

useの引数で「-base」

「まぁちょっと違うことやっていますよ」っていうサインのためにも、子クラスで親クラスを継承するときに変わった書き方が必要になってきます。そこで、たまに目にする「use Class qw/-base/」っていうのをサインにしようじゃないかと思いました。

既存研究

importを使う手法はExpoterモジュールが最初からサポートしていて、以下の記事から詳しく学べます。

これと合わせて、「Module::Pluggableを利用」の話で紹介したtokuhiromさんのハックと、cloudforcastのCloudForcast::Dataクラスのimport関数のコードが参考になります。

試してみた方法

以上をまるっと参考にすると、以下のように実装できました。

#親クラス
package Calc;
use strict;
use warnings;
use utf8;
use Smart::Args;
use Sub::Identify;

sub import {
    my ($parent, $name) = @_;
    my $child = caller(0);

    no strict 'refs';
    if ( $name && $name =~ /^-base/ ) {
        if ( ! $child->isa($parent) ) {
            push @{"$child\::ISA"}, $parent;
        }

        for my $func (sort keys %{"${parent}::"}) {
            next if $func =~ /^(?:BEGIN|CHECK|END)$/;
            next if $func =~ /^_/;
            my $code = *{"${parent}::${func}"}{CODE};
            next unless $code;
            next if $parent eq Sub::Identify::stash_name($code);

            *{"$child\::$func"} = $code;
        }

        strict->import;
        warnings->import;
        utf8->import;
    }
}

#以下同上

#子クラスAの場合
package Calc::Addition;
use Calc qw/-base/;#parentは使わない

sub calc {
    args_pos my $self,
            my $inp_a => 'Int',
            my $inp_b => 'Int';

    $self->{_result} = $inp_a + $inp_b;
}
1;

ついでに、strictとかwarningsとかutf8までエクスポートしちゃって、とても楽チンですね!

まとめ

結論的には、子クラスで「use Class qw/-base/」して、親クラスのimportをいじるという第三番目の手法がベストだと思います。(ちょっと違うよ!!っていうのがあったらどんどんご指摘頂ければです!)

すること

  • 子クラスにuse Class qw/-base/;
  • 親クラスのimportをいじる

できること

  • 親クラスの継承
  • 親クラスにエクスポートされている関数の子クラスでの利用
  • strict,warnings,utf8とかも委譲
  • なんか違うことやっているなーっていうサインを出す

できなくなること

  • 子クラスで親クラスにエクスポートされいる関数名のメソッドを作れなくなる

Exporter::Base

で、上記のコードをちょっといじって、そして、Exporter::Baseというかたちでモジュールにしてみました。どうでしょうか?とりあえず、モジュール名がいけていないとか、コメント頂ければありがたいです。このままだとCPANにあげるのはどうかなーと思うんだけど、まぁコメントほしさと、修正する原動力確保にPrePANにあげてみようかなー。

Perl hachioji.pm update_at : 2013-05-31T15:10:49
hirobanex.netの更新情報の取得
 RSSリーダーで購読する   
blog comments powered by Disqus