― Web Technology and Life ―

Containerという名のPerlモジュール群

2011-12-10
Kamui::Containerを使って以来、Containerを使うのが大好きなんですが、とはいえ、Containerってなんなのかって言われるとよくわからないなーと思って、いろいろ調べたのでその結果を詳細に共有します。

Containerとは

端的に言えば、GoFのオブジェクトデザインパターンのFlyweightパターンに該当するオブジェクトです。

より詳細に言うと、コンストラクションされたインスタンスやリファレンスをキャッシュしておくことで、いろんなクラスからそのキャッシュしたオブジェクトを利用するような形態のオブジェクト、あるいは、そのクラスを生成をサポートするモジュール群、を指すといえると思います。

ただ、Container自体あまり一般的ではないと思うので、ここで「ContainerとはPerlのFlyweightパターンの実装である」ということしたいと思います。

メリット

Containerを利用すると以下のようなメリットを享受できます。

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

デメリット

一方で、一般的なContainerの利用方法では以下のようなデメリットをうけることがあります。

  • どこからでもアクセスできるグーバル変数化によるスコープ無限拡大
  • インスタンスの中身が変化するタイプのインスタンスには向かない

Containerの利用例

ぐだぐた言ってもよくわからないので、早速利用例をみてみたいと思います。とりあえず、一番一般的なObject::Containerのサブクラス利用で、LWP::UserAgentとかDBIとかを使ってみましょう。

Containerを使ってコード書く場合

#サブクラスでインスタンスの登録
package OreOre::Container;
use strict;
use warnings;
use Object::Container '-base';
use LWP::UserAgent;
use HTTP::Cookies;
use DBI;

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,
    );
};


register db => sub { 
    my $dbh = DBI->connect('dbi:mysql:dbname','root','pass', {
        RaiseError        => 1,
        PrintError        => 0,
        mysql_enable_utf8 => 1,
    });
};
1;

#メインスクリプト
#!/usr/bin/env perl
use strict;
use warnings;
use OreOre::Container qw/obj/;

my $res = obj('ua')->get('http://example.com/');

obj('db')->do(
    q{
        INSERT INTO response ( status,html ) VALUES (?,?);
    },
    $res->code,$res->content
);

なんかすごいすっきりしていて、いいと思いませんか???

Containerを使わずにコードを書く場合

いまいちピンと来ない人のために、Object::Containerを使わない場合の例を書いてみたいと思います。まぁ、がんばって書いても以下みたいな自分のコードの中でラッパーのようなものを書いてそこから利用すると思うんですね、例えばこんな感じ。

#オレオレLWP::UserAgentラッパー
package OreOre::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;

#オレオレDBIラッパー
package OreOre::DB;
use strict;
use warnings;
use DBI;

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

    my $dbh = DBI->connect('dbi:mysql:dbname','root','pass', {
        RaiseError        => 1,
        PrintError        => 0,
        mysql_enable_utf8 => 1,
    });
}
1;

#メインスクリプト
#!/usr/bin/env perl
use strict;
use warnings;
use OreOre::DB;
use OreOre::UA;

my $res = OreOre::UA->instance->get('http://example.com/');

OreOre::UA->instance->do(
    q{
        INSERT INTO response ( status,html ) VALUES (?,?);
    },
    $res->code,$res->content
);

ふたつくらいならまぁラッパークラスを作っておいてもいいですけど、なんか無駄にクラス増えるのも嫌だし、なんかuse一般するもだるいし、毎回コンストラクションしているもの正直いらないしとかとか上述したメリットを享受できないためにいろいろな無駄があると思います。

いろいろなCPANにあるContainerモジュールとそれぞれの機能

Object::Containerをサブクラスで普通に使うと上述したようにデメリットにぶちあたりますし、いろいろな要件があると思うので、ここの要件に合わせたContainerの実装をしたいですよね?!ここでは、その参考のためにCPANにあがっているContainerモジュールを三つ比較して、Containerにはどんな機能があるか洗い出したいと思います。

モジュール別機能比較一覧

まずはじめに、機能一覧と各モジュールの実装状況のまとめを以下に示したいと思います。

機能 Object::Container Object::Container::Exporter Pickles::Container
オブジェクト型インターフェース ×
サブクラス型インターフェース
Singleton化によるキャッシュ化 ×
Singleton化によらないキャッシュ化 ×
1プロセス内でのキャッシュ管理選択オプション × ×
キャッシュ化の遅延実行
キャッシュ化の事前実行オプション × ×
可変のショートカットメソッドの提供 ×
ショートカット指定の特定dir内のクラスをキャッシュ化 × ×

基本的には各PODを読めばいいと思うんですが、以下ではちょっとわかりづらい機能や注意したいことをあげていきたい思います。

インターフェースについて

個人的には、上述した例のようなContainerモジュールを継承したクラスを作成するサブクラス化インターフェースがあればいいんじゃないかな?と思うんですが、以下のようなオブジェクト型のインターフェースもあったりします。

use Object::Container;

# initialize container
my $container = Object::Container->new;

# register class
$container->register('HTML::TreeBuilder');

# register class with initializer
$container->register('WWW::Mechanize', sub {
    my $mech = WWW::Mechanize->new( stack_depth => 1 );
    $mech->agent_alias('Windows IE 6');
    return $mech;
});

# get object
my $mech = $container->get('WWW::Mechanize');

Singleton化について

SingletonとはGoFのデザインパターンであげられているデザインパターンで、あるオブジェクトがひとつであることを保証してくれるものです。デメリットにあげたように、「どこからでもアクセスできるグーバル変数化によるスコープ無限拡大」がおこり、本来使用するべきではないところからもアクセスできてしまうし、実際、どこで使っているかよくわからなくなるということが起きます。

興味深いのが、Pickles::Containerです。こちらは、サブクラスインターフェスを保ちつつ、Singleton化しないようにすることになっています。ただ、WAFのPicklesのクラスの一つなので、Pickles全体でうまくサブクラス化するような書き方していますが、気軽に他のWAFにくっつけたり、他のサービスに実装するのは、ちょっと凝った書き方をすることになります。

1プロセス内でのキャッシュ管理

1プロセス内でのキャッシュというのは、永続環境にあるworkerやserver等の場合、1回のアクセス内による複数インスタンス呼び出しが効率化される一方で、2回目のアクセスではまたインスタンスの生成から始められます。一方で、Containerをサブクラスで普通に使うと、プロセス間キャッシュとなり、1回目のアクセスによってキャッシュされたインスタンスは、2回目のアクセスでも生きておりその後も1回目のアクセスで生成されたインスタンスが使用され続けられます。

このような仕組みになるので、Singletonと同様に、デメリットにあげた「どこからでもアクセスできるグーバル変数化によるスコープ無限拡大」を防ぎたいということで、まず、1プロセス内だけキャッシュ化させておきたいという要件を満たす必要もあったりします。「どんなときにそんなことしたいの?」って思う方は、『Scope::Containerでリソース管理を行う 』をご覧ください。

このkazeburoさんのScope::Containerを使えば、Pickles::Container以外でも、スコープ管理できるんじゃないかなーと思っているんですけど、個人的にまだ必要になっていないのでチェックしていません、すみません。。。

キャッシュ化の遅延実行

基本的には、どのContainerも、obj('ua')とはじめて呼ばれたときに、コンストラクションがおこなわれてインスタンスがキャッシュされます。

Object::Container::Exporterについては、load_classメソッドがあり、キャッシュ化するオブジェクトのモジュールのuseもこの時におこなえるようになっています。この遅延実行のuseは、Makfile.PLをちゃんと書いてなかったり、テスト書いていなかったリスト、本番環境でモジュール足りなかったみたいことが起きる原因になるので、そのあたりは注意し、やるべきMakfile.PLの管理やテストを書くということはちゃんとしましょう。

可変のショートカットメソッドの提供

以下のような切り替えができるということですね。

  • use MyContainer qw/obj/って書いて、obj('ua')って呼び出す
  • use MyContainer qw/con/って書いて、con('ua')って呼び出す

ショートカット指定の特定dir内のクラスをキャッシュ化

もうここまで読んでいる人、たぶんいないと思うんだけど、ぶっちゃけここを強調したくてここまで書いてきましたwこれは、


#ディレクトリ構成
`-- MyApp
      |-- Api
      |   `-- User.pm
      `-- Cmd
      |   `-- Password.pm
      `-- Container.pm

こんなディレクトリ構成があったとして、

use MyApp::Container qw/api cmd/;

my $hash_val = cmd('Password')->generate($pass);

my $row = api('User')->register(
    id   => 'nekokak',
    pass => $hash_val,
);

こんな風に、ディレクトリApi以下のクラスにアクセスするapi関数のエクスポートと、その中のUserクラスのオブジェクトキャッシュをすることが可能になる機能です。

見出しをわけて詳しく解説したいと思います。

Object::Container::Exporterのよさ

Containerのデメリットに、「インスタンスの中身が変化するタイプのインスタンスには向かない」と書きかましたが、Object::Container::Exporterを使うと楽にその解決方法を提供しますし、とてもいいこと満載です。具体的には、以下のことから解放されます。

  • 自作サービス内のクラスのuseとnewから解放される
  • だらだらuseを書き続けることから解放される
  • オブジェクトが書き変わる系のモジュールもラップすればContainerで楽に使える
  • いちいちContainerにregisterすることからも解放される

具体的にコードを示します。


#ディレクトリ構成
`-- MyApp
      |-- Api
      |   `-- User.pm
      `-- Cmd
      |   `-- Password.pm
      `-- Container.pm

package MyApp::Container;
use Object::Container::Exporter -base;

register db => sub { 
    my $self = shift;
    $self->load_class('DBI');

    my $dbh = DBI->connect('dbi:mysql:dbname','root','pass', {
        RaiseError        => 1,
        PrintError        => 0,
        mysql_enable_utf8 => 1,
    });
};

1;

package MyApp::Api::User;
use MyApp::Container;

sub new {bless{},+shift}

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

    #MyApp::Containerをuseするとデフォルトではcontainerという関数を
    #エクスポートしてregisterしたオブジェクトにアクセスできます
    container('db')->do(
        q{
            INSERT INTO response ( name,pass ) VALUES (?,?);
        },
        $args->{name},$args->{pass}
    );

}
1;

package MyApp::Cmd::Password;
use Crypt::SaltedHash;

sub new {bless{},+shift}

sub generate {
    my ($self,$pass) = @_;

    my $crypt = Crypt::SaltedHash->new();

    $crypt->add( $pass );

    my $salted = $crypt->generate();
}
1;

こんな感じのディレクトリ構成とアプリ内クラスを想定して、Object::Container::Exporterを使った場合と、そもそもContainerを使わなかった場合でメインコード比較したいと思います。

そもそもContainerを使わなかった場合

package MyApp::Cmd::Password;
package MyApp::Api::User;

my $passworder = MyApp::Cmd::Password->new;

my $hash_val = $passworder->generate($pass);

my $obj_user = MyApp::Api::User->new;

my $row = $obj_user->register(
    id   => 'nekokak',
    pass => $hash_val,
);

この場合は、newするひつようはないんですが、newするケースを想定するとこれくらいのコードになります。

Object::Container::Exporterを使った場合

use MyApp::Container qw/api cmd/;#useはこれだけで済んじゃう

#Containerに登録しづらいモジュールをラップしたクラスを手軽にアクセスできる
my $hash_val = cmd('Password')->generate($pass);

my $row = api('User')->register(
    id   => 'nekokak',
    pass => $hash_val,
);

すっきりですね。「そもそもContainerを使わなかった場合」のコードを想定するともはや冗長ですね。

自分のクラスの呼び出しもuseしてnewするという悪夢から解放されますし、クラスの切り分けを上手にすればそれぞれのコードの可読性も上がると思います。

一応、Object::Container普通に使ってそこに詰め込んでいけばいいじゃんってつっこみもありますが、現状のObject::Containerではobjみたいなエクスポートさせる関数が一個しかつくれないので、ちょっとわかりづらいかなーという気がしていますので、個人的には、Object::Container::Exporterの方が好きです。

Object::Container::ExporterとKamui::Containerの違い

Object::Container::Exporterはnekokakさんがもともと自作WAFのKamuiで実装していてたKamui::Containerを切り出してCPANにアップしたものなので、ほぼ同じものです。ただ、KamuiのWAF的な以下の機能がObject::Container::Exporterにはありませんし、

  • 実行しているカレントディレクトリのPath::Classオブジェクトcontainer('home')の自動設定
  • config.pl内のデータをとれるcontainer('conf')の自動設定

逆に、Kamui::Containerには、デフォルトのサブクラス化したContainerクラスにキャッシュしたオブジェクトを呼び出すときにcontainerというデフォルトの関数名をかえることができなかったりと、ちょっとした違いがあります。

最後に

とりあえず、「Perl Advent Calendar Japan 2011 Casual Track」用にライトにContainerの紹介を書いたんだけど、そちらはあくまでCasual Trackなのであまり詳しく小難しい話をだらだら書いてもしょうがないなーと思って短くしちゃったんで、せっかく調べたことを別にまとめておきたいなーと自分のブログに詳しく載せてしまいました。

オブジェクトデザインパターン的な視点からContainerを考えてみた記事も以前に書いたのでそちらも合わせて参考とかしてもらえるとうれしいです。

また、間違いだったり、こうじゃないかなーだったり、ツッコミなどなどあれば、コメント、tiwtter等でどしどしご指摘いただけるとうれしいです!

【追記】
typo修正しましたー。kitsさんありがとうございます!

Perl update_at : 2011-12-17T11:20:13
hirobanex.netの更新情報の取得
 RSSリーダーで購読する   
blog comments powered by Disqus