― Web Technology and Life ―

PerlのCPANモジュールPickles::ContainerにみるFlyweightパターン

2011-09-19
『PerlのCPANモジュールに学ぶオブジェクトデザインパターン』第3弾としてPickles::ContainerからFlyweightパターンを読み取ります。

問題:毎回同じインスタンスを生成している

前回の『PerlのCPANモジュールObject::ContainerにみるSingletonパターン』で述べた問題と同じような問題を想定します。

せっかくなので、今回は具体例を厚くしてみます。例えば、オレオレ環境に即したHTTPクライアントとしてLWP::UserAgentの設定を書いて、いろんなところで使いたいと思います。

#search.cpan.orgの検索結果を取得するクラス
package Scraper::SerpSearchCPAN;
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Cookies;

sub get {
    my ($self,$query) = @_;

    $ENV{'HTTP_PROXY'} = 'http://hogehoge.proxy.com:80';
    my $cookie_jar = HTTP::Cookies->new(file => $params->{cookie}, autosave => 1);

    my $ua = LWP::UserAgent->new(
        agent      => 'mogemoge_crawler',
        timeout    => 300,
        max_size   => 1000000,
        cookie_jar => $cookie_jar,
        env_proxy  => 1,
    );

    my $response = $ua->get('http://search.cpan.org/search?query='.$query);
}
1;
#chiebukuro.search.yahoo.co.jpの検索結果を取得するクラス
package Scraper::SerpChiebukuro;
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Cookies;

sub get {
    my ($self,$query) = @_;

    $ENV{'HTTP_PROXY'} = 'http://hogehoge.proxy.com:80';
    my $cookie_jar = HTTP::Cookies->new(file => $params->{cookie}, autosave => 1);

    my $ua = LWP::UserAgent->new(
        agent      => 'mogemoge_crawler',
        timeout    => 300,
        max_size   => 1000000,
        cookie_jar => $cookie_jar,
        env_proxy  => 1,
    );

    my $response = $ua->get('http://chiebukuro.search.yahoo.co.jp/search?p='.$query);
}
1;

さすがにこんな重複したコードをだらだら書くこととはなく、以下のようにUAのクラスを作ってそれを再利用していくと思います。

#UAのクラス
package Scraper::UA;
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Cookies;

sub instance {
    my ($self) = @_;

    $ENV{'HTTP_PROXY'} = 'http://hogehoge.proxy.com:80';
    my $cookie_jar = HTTP::Cookies->new(file => $params->{cookie}, autosave => 1);

    my $ua = LWP::UserAgent->new(
        agent      => 'mogemoge_crawler',
        timeout    => 300,
        max_size   => 1000000,
        cookie_jar => $cookie_jar,
        env_proxy  => 1,
    );
}
1;
#Scraper::UAを利用してましになったScraper::SerpSearchCPANクラス
package Scraper::SerpSearchCPAN;
use strict;
use warnings;
use Scraper::UA;

sub get {
    my ($self,$query) = @_;
        
    my $response = Scraper::UA->instance->get('http://search.cpan.org/search?query='.$query);
}
1;

なかなかシンプルになりました。しかし、見た目はシンプルでも、「Scraper::UA->instance」を呼ぶたびにLWP::UserAgentのnewも走るし、CPUとメモリの観点からはもうひと工夫したいところです。

Flyweightパターンで解決

先ほどの問題は、端的に言えば、一回生成すればよいインスタンスを何度も生成することで、同じコードが何回も走り、同じ内容のインスタンスで別に生成されてメモリを圧迫することが問題でした。

Flyweightパターンとは

Flyweightパターンは、オブジェクトを多数生成するときに発生するコストを減らすために使うパターンです。使う目的とか、メリットという点は上であげたような問題の対処が多くSingletonパターンとほとんど同じだと思うのですが、違いとしては、Singletonパターンが唯一のインスタンスを保証するという一方、Flyweightパターンはインスタンスを管理する枠組みを提供するだけで場合によってはインスタンスが複数生成される可能性を保持するという点で異なります。最近良くみられるコンテナーの場合、Singletonパターンの実装というよりFlyweightパターンの実装がメインと言えるでしょう。

それでは、コンテナーのひとつのPickles::Containerで、どのようにFlyweightパターンが実装されているか確認したいと思います。

CPANモジュールPickles::ContainerにみるFlyweightパターン

Pickles::ContainerはPerlのウェブアプリケーションフレームワークPicklesで使用されているインスタンス管理モジュールです。とりあえず、ややこしいのですが、いちから使い方を確認しながらFlyweightパターンの実装をみてみます。

まず、Pickles::Containerを継承したScraper::Containerを用意します。

#package Scraper::Container;
use base qw(Pickles::Container);
1;

次に、メインコード(ここではScraperという実行クラスを用意)のどこかで、Scraper::Containerをuseしてregisterメソッドでインスタンスの登録を行います。

use Scraper;
use strict;
use warnings;
use Scraper::Container;

sub run {
    
    $self->init;
    
    $self->work;
}

sub init {
    my $self = shift;
    
    $container = Scraper::Container->new;
    
    #uaのインスタンス生成コードを登録
    $container->register(ua => sub {
        $ENV{'HTTP_PROXY'} = 'http://hogehoge.proxy.com:80';
        my $cookie_jar = HTTP::Cookies->new(
            file     => $params->{cookie}, 
            autosave => 1
        );

        my $ua = LWP::UserAgent->new(
            agent      => 'mogemoge_crawler',
            timeout    => 300,
            max_size   => 1000000,
            cookie_jar => $cookie_jar,
            env_proxy  => 1,
        );
    });
    
    my $self->{contianer} = $container;

}

sub work {

    while(1){
        #永続的な処理
    }
}

Scraper::Containerのregisterメソッドは以下のようになっています。コードリファレンスの場合は、即座にインスタンスを生成してキャッシュするのではなくて、一旦、コードリファレンスとして登録して、実際に使うときにインスタンスを生成する仕組みになっています。

#$nameが'ua'
#$componentがLWP::UserAgentのインスタンスを生成しているコードリファレンス
#$optsは今回はなし

sub register {
    my ($self, $name, $component, $opts) = @_;

    if (ref $component eq 'CODE') {
        $opts ||= {};
        my %data = (
            %$opts,
            initializer => $component,
        );
        $self->components->{ $name } = \%data;
    } else {
        $self->objects->{$name} = $component;
    }
}

実際には、以下のようにPickes::Containerのgetメソッドでインスタンスを取り出して使います。

#Pickes::Container利用したScraper::SerpSearchCPANクラス
package Scraper::SerpSearchCPAN;
use strict;
use warnings;

#どこかでこのnewが呼ばれて、上で生成されたcontainerがキャッシュされている前提
sub new {
    my ($class,$args) = @_;

    bless { 
        container => $args->{container}
    },$class;
}

sub get {
    my ($self,$query) = @_;
        
    my $response = $self->{contianer}->get('ua')->get('http://search.cpan.org/search?query='.$query);
}
1;

Pickes::Containerのgetメソッドは以下のように実装されています。


sub get {
    my ($self, $name, @args) = @_;

    #先にオブジェクトが生成されていれば、キャッシュから手に入れる
    my $object = $self->{objects}->{$name} || $self->{scoped_objects}->{$name};

    #キャッシュされていなかった場合
    if (! $object) {

        #インスタンスを生成するコードリファレンスを取り出す
        my $data = $self->components->{ $name };
        #インスタンスを_construct_objectメソッドで生成して取り出す
        $object = $self->_construct_object($data, @args);
        if ($object) {
            #persistentオプションが指定されていれば通常のところにキャッシュし、
            #そうでなければscope限定のところにキャッシュする
            if ($data->{persistent}) {
                $self->objects->{$name} = $object;
            } else {
                $self->scoped_objects->{$name} = $object;
            }
        }
    }
    return $object;
}

コメントを入れたとおりですが、このgetメソッドで、一度キャッシュされたインスタンスはそのまま返し、キャッシュされていなかったインスタンスは生成してキャッシュに入れてから返しています。ここが、FlyWeightパターンの勘所です。

簡単なFlyweightパターンの実装

Pickles::Containerは、遅延ロード(インスタンス生成コードを最初に走らせない)とか、スコープ限定で使えるようにするとか、いろいろな要件が入っているので、ややこしくなっていますが、スコープとかを考えずScraper::UAを変更してシンプルに書けば以下のようになります。

#UAのクラス
package Scraper::FlyweightContainer;
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Cookies;

#いろんなインスタンスをキャッシュできるようにメソッド名は、インスタンス名にする
sub ua {
    my ($self) = @_;

    $self->{ua} ||= do {
        $ENV{'HTTP_PROXY'} = 'http://hogehoge.proxy.com:80';
        my $cookie_jar = HTTP::Cookies->new(file => $params->{cookie}, autosave => 1);

        my $ua = LWP::UserAgent->new(
            agent      => 'mogemoge_crawler',
            timeout    => 300,
            max_size   => 1000000,
            cookie_jar => $cookie_jar,
            env_proxy  => 1,
        );
    };
}

#せっかくだから、Qudoのインスタンスもキャッシュできるようにしてみる
sub qudo {
    my ($self) = @_;

    $self->{qudo} ||= Qudo->new(
        driver_class => 'Skinny',
        databases => [
            +{
                dsn      => 'dbi:SQLite:/tmp/qudo.db',
                username => '',
                password => '',
            }
        ],
    );
}

1;

要するに、「||=」を使用したメソッドキャッシュでFlyweightパターンっぽいものは実装可能です。

まとめ~使いどころ、メリット/デメリット~

メリット、デメリットはSingletonパターンとほぼ同じです。

メリット

  • 都度インスタンス生成するCPUコストの削減
  • 無駄なインスタンス生成によるメモリの削減
  • 同じコードの繰り返しによるコードのメンテナンス性の低下を防止
  • 同じコードの繰り返し書かないことによる開発スピードの向上

デメリット

  • インスタンスの中身が変化するタイプのインスタンスには向かない

Pickles::Containerの場合は、1プロセス内のみ有効なキャッシュを行うpersistentオプションがついていますが、それでもあまりメリットを享受できないので、インスタンスの中身が変化するタイプのモジュールにはむかないでしょう。また、Singletonパターンのときは述べ忘れましたが、万一のメモリリークがキャッシュを行うことによって可能性が残り続けてしまうというもの挙げられます。いろんなモジュールに依存しまくっているとどこでメモリリークが起きているのか把握するのは難しいというのは定番の問題ですが、この問題についてとくに気を付けなくてはいけなくなります。

Singletonパターンとの違い

大きな違いとしては、Singletonパターンの場合にあった「どこからでもアクセスできるグーバル変数化によるスコープ無限拡大」という問題がなくなるということです。ここで紹介したPickles::ContainerのようにあえてSingletonパターンを採用しない限り、この問題には該当しません。一方で、Object::Containerでできたような、use Object::Container qw/obj/;して他のクラスから軽々と生成したインスタンスを取り出すように芸当を行うと、use Object::Container qw/obj/;したクラス毎にそれぞれgetするインスタンスを生成してしまうので、そういう実装はSingletonパターンなしにはできません。Pickles::Contanerの場合のように、一度どこかで生成したインスタンスを引き継いで使用しなくてはいけないのです。

最後に

SingletonパターンとFlyweightパターンの違いをみるのに、Pickles::Containerはうってつけでした。最初、両者の違いがいまいひとつピンとこなかったのですが、Pickles::ContainerとObject::Containerの違いをみてよくわかりました。いろいろなモジュールのコードを読むというのは重要ですね!!(といってもわりと流し読みなので、変な所があれば随時ご指摘いただきたいです!)。

あと、具体例を厚くすると書いている私の理解度は増すのですが、やはりダラダラ長くなって記事としては読みづらいものになってしまいますね・・・。まぁ、そんなもんですね!!また、SingletonパターンとFlyweightパターンの違いを見ながら、そろそろContainerの要件がわかってきたので、Object::ContainerとPickles::Contanerの違いは別に近いうちまとめたいと思っています。

Perl update_at : 2011-09-19T16:37:07
hirobanex.netの更新情報の取得
 RSSリーダーで購読する   
blog comments powered by Disqus