天然パーマです。

Object::Containerを応用したModel呼び出し

各種Model群についてObject::Containerを応用しつつ管理するとカジュアルにシングルトンになって効率いいかも!って思って、手元の小さなWebアプリで実装してみた。実験的にやってるんで、これおかしいって点あるかもなんでその場合はお手柔らかにツッコんでくだされ。

だいたい僕はこんな感じでWebアプリのファイル、クラス構成をとっています。

./
└── MyApp
    ├── Model
    │   └── Entry.pm
    ├── Model.pm
    ├── Web
    │   └── Controller
    │       └── Root.pm
    └── Web.pm

うんで、Controllerからはtokuhiromからよく「Catalystっぽいよねー」って言われているけど頑なに$self->model('Entry')のようなインターフェースでModelへアクセスしています。

package MyApp::Web::Controller::Root;
use Mojo::Base 'Mojolicious::Controller';

sub index {
    my $self = shift;
    my $entries = $self->model('Entry')->get_recent_entries();
    $self->stash->{entries} = $entries;
    $self->render();
}

1;

呼ばれるはずのMyApp::Model::Entryには呼び出されるはずのメソッドが定義されています。

package MyApp::Model::Entry;
use Mouse;

sub get_recent_entries {
    ...;
    return $entries;
}

1;

ここまでは個人的にいいとして、Modelをどのようにロードさせるかって話です。Object::Containerを使った今回の例ではまず、MyApp::Loader::Modelをつくりました。

package MyApp::Loader::Model;
use Mouse;
use Module::Load qw//;

has 'instances' => ( is => 'rw', isa => 'HashRef[Object]', default => sub { +{} } );

sub load {
    my ($self, $name) = @_;
    my $instances = $self->instances;
    return $instances->{$name} if $instances->{$name};
    my $class = "MyApp::Model::" . String::CamelCase::camelize($name);
    Module::Load::load($class);
    my $model = $class->new;
    $instances->{$name} = $model;
    $self->instances($instances);
    return $class;
}

__PACKAGE__->meta->make_immutable();

動的に名前で指定されたModelをロード。生成したModelのインスタンスをinstancesプロパティで保持しています。そしていよいよObject::Cotainerが登場します。このLoaderをシングルトンとして提供出来るようにします。

package MyApp::Model;
use Object::Container '-base';
use MyApp::Loader::Model;

register 'model' => sub {
     return MyApp::Loader::Model->new;
};

1;

なんか簡潔にするために他のコードを省いているんで無駄にクラス分けすぎ感ありますが... 最後にはMojoliciousなMyApp::Webでhelperの定義をします。

package MyApp::Web;
use Mojo::Base 'Mojolicious';
use MyApp::Model;

sub startup {
    my $self = shift;
    $self->helper(
        model => sub {
            my ($c, $name) = @_;
            return MyApp::Model->get('model')->load($name);
        }
    );
...;

するとControllerから$self->model('Entry')とかで二度目からはキャッシュされたModelインスタンスが取得出来ます。基本的にMyApp::ModelのgetメソッドからModelを取得することさえ守れば問題ないかな〜。と、まぁ昨日のエントリに引き続きWebアプリの構造と実装をどうするかを考える最近でございます。

* 追記

tokuhiromのご意見エントリー!