― Web Technology and Life ―

【Amon2のオレオレTips】MVCを意識したModelの具体的な実装とその考察

2013-01-06
前回、『【Amon2のオレオレTips】MVCを意識したModelの実装要件』と題して、コマンドラインからも動かせるようなModel実装の要件について述べましたが、今回は、その実装方法を具体的に2つほど例を上げてみたいと思います。その中のContainerを使った「特定のクラス配下にクラスを用意」というのが今回のオレオレTipsです。

Modelクラスの実装における前提

まず、大前提ですが、基本的にModelの大部分を占めるはデータベース周りであり、その実装にあたっては、生でDBIを使うというよりは、O/Rマッパーを使ってしまったほうが楽であり、現在ではTengあたりを使用するのがいいんじゃないのかなーというのが前提です。(そうじゃない場合も、クラスとしては、MyApp::Model::DBとかMyApp::Model::DataStrageとかってクラスを、"まず"作るという感じですね)

また、Amon2で実装されるサンプルアプリをMyAppという名前空間を使うこととします。

想定される具体的な実装例の比較

さて、早速の実装例ですが、まず、サマリで2つの実装例の概要とメリット/デメリットを比較してみます。

手法 概要 メリット デメリット
DBクラスを拡張 Tengクラスを継承したMyApp::Model::DBみたいなクラスに、やりたい操作をメソッドではやす。 MyAppクラスに、sub db { }というメソッド作ってメソッドキャッシュし、$c->db->fetch_userという感じで呼び出せる。TengのRowオブジェクトにもメソッドを生やしていくことが前提になる。シンプル Amon2に限らないけど、Tengのメソッドなのか、独自実装したメソッドなのかわかりづらい。また、DBクラスが巨大になる。
特定のクラス配下を拡張 MyApp::Model::Api::のようなディレクトリ配下に自由にのクラスを作って、$c->api('User')のようなに呼び出すように実装。 $c->api("User")のようなコードは一見しで独実装だとわかりやすく、コードが追いやすい気がします。 TengのRowクラスの存在意義が微妙に。そんな中で、Rowクラス拡張をすると、Rowクラスになんか実装があるという意識がなく、ハマった時に困るかも。

Tengをかます場合は、Rowクラスをどうするかで一長一短という感じだと思っているんですが、どうでしょうか?SQL系のDBを使わない場合とか、よく知らなんで、そういう場合はどうなるのかなーと思ったりするのですが、なかなかNoSQL的なやつを使うという状況にも出くわさないので、そんなケースにも出会ってみたいなーとか思ったりする次第です。

「DBクラスを拡張」の実装コード

次に個別の実装例別にサンプルコードを、DBクラス、MyAppクラス別、Dispatcherクラス、テストコードなどを、示していきたいと思います。

MyApp::Model::DBクラス

package MyApp::Model::DB;
use parent 'Teng';
use Smart::Args;

__PACKAGE__->load_plugin('Count');
__PACKAGE__->load_plugin('FindOrCreate');
__PACKAGE__->load_plugin('Pager');
__PACKAGE__->load_plugin('SQLPager');

sub fetch_user {
    args my $self,
         my $user_id;

    my $row = $self->single('user',{ id => $user_id });

    return $row;
}

1;

MyAppクラス

package MyApp;
use strict;
use warnings;
use utf8;
use parent qw/Amon2/;
our $VERSION='0.01';
use 5.008001;

use MyApp::Model::DB;

sub db {
    my $self = shift;

    if ( !defined $self->{db} ) {
        my $conf = $self->config->{'DB'}
          or die "missing configuration for 'DB'";

        $self->{db} = MyApp::Model::DB->new(connect_info => $conf);
    }

    return $self->{db};
}
1;

MyApp::Web::Dispatcherクラス

package MyApp::Web::Dispatcher;
use strict;
use warnings;
use utf8;
use Amon2::Web::Dispatcher::Lite;

any '/' => sub {
    my ($c) = @_;
    return $c->render('index.tt');
};

get '/user/status/:user_id' => sub {
    my ($c,$args) = @_;

    my $row = $c->db->fetch_user($args);

    return $c->render('user_status.tt',$row->get_columns);
}
1;

MyApp::Model::DBクラスのテストコード

use strict;
use warnings;
use utf8;
use t::Util;
use Test::More;
use MyApp;
use Test::Deep;
use Test::Deep::Matcher;

DBのセットアップとかsetup的なゴニョゴニョ
my $user_id = ...;

my $c = MyApp->bootstrap;

my $row = $c->db->fetch_user($user_id);

cmp_deeply ($row->get_columns, +{
    id   => is_number,
    name => is_string,
    age  => is_number,
    sex  => is_string,
});

DBを消したりなどのゴニョゴニョ

done_testing;

Containerを使ったオレオレな「特定のクラス配下にクラスを用意」の実装コード

こちらは類似コードをissmさんが、Amon2::Plugin::Modelと紹介していただきました。こっちのほうがスマートですねー、というよりこれが一番Amon2的にはスマートかな(苦笑)

ここまできたので、とりあえず、オレオレのContainerを使ったやり方ですが、Web周りのオブジェクト管理は、Amon2の機構を利用することにして、コマンドライン周りのオブジェクト管理はObject::Container(私は、Object::Container::Exporterというのを使っていますが)などに任せます。

Amon2クラスもオブジェクトキャッシュを考えられた仕組みになっているので、Amon2クラスと別のキャッシュシステムの導入には意図しない罠が潜む余地あるので、なんとなく微妙な感があるような気がしていますが、個人的にこっちが慣れているし、誰彼構う環境にはないので、オレオレでやちゃっていますw

MyApp::Containerクラス

package MyApp::Container;
use strict;
use warnings;
use Object::Container::Exporter -base;
use Amon2::Declare;

register_default_container_name 'obj';

register_namespace api  => 'MyApp::Model::Api';

register conf => sub { c()->config };

register db => sub {
    my $self = shift;
    $self->load_class('MyApp::Model::DB');
    MyApp::Model::DB->new( connect_info => c()->config->{'DB'});
};
1;

ちなみに、最初、いちいち、テストスクリプトにせよ、実行スクリプトにせよ、MyApp->bootstrapするのが、めんどくさかったので、use MyApp; sub c {MyApp->context || MyApp->bootstrap}としていたんですが、Amon2的にはそういう使い方を想定していないということで、今後のバグの温床にならないようにスクリプト別か、共通化できるところ(テストのt::Utilとか)で、Amon2->bootstrapを呼ぶようにしました。あと、結局、c()->configをconfでキャッシュしているのが、微妙ですね。。。

MyApp::Model::DBクラス

package MyApp::Model::DB;
use parent 'Teng';
use Smart::Args;

__PACKAGE__->load_plugin('Count');
__PACKAGE__->load_plugin('FindOrCreate');
__PACKAGE__->load_plugin('Pager');
__PACKAGE__->load_plugin('SQLPager');

1;

MyApp::Model::Api::Userクラス

package MyApp::Model::Api::User;
use MyApp::Container;
use Smart::Args;

sub new {bless +{},+shift}

sub fetch {
    args my $self,
         my $user_id;

    my $row = obj('db')->single('user',{ id => $user_id });

    return $row;
}
1;

MyAppクラス

package MyApp;
use strict;
use warnings;
use utf8;
use parent qw/Amon2/;
our $VERSION='0.01';
use 5.008001;

use MyApp::Container qw/-no_export/;

sub obj { MyApp::Container->instance->get($_[1]) }

##ここは非常に良くない書き方で、どうしようか悩み中
sub api { MyApp::Container->instance->{_register_namespace}->{api}->($_[1]) }
sub cmd { MyApp::Container->instance->{_register_namespace}->{cmd}->($_[1]) }

1;

Object::Container::Exporterに、instanceメソッドから予め登録したnamescapeの関数にアクセスする方法がないなーと思って、非常によろしくない書き方になっていますが、まぁこんな感じとうことで。。。Object::Container::Exporterにパッチを送ろうか、それとも、どうにか出来る方法があるのか、こういう名前空間とかをいろいろとゴニョゴニョする系のモジュールは、見ていて混乱してくるので、ちょっと不安になりますね(苦笑

MyApp::Web::Dispatcherクラス

package MyApp::Web::Dispatcher;
use strict;
use warnings;
use utf8;
use Amon2::Web::Dispatcher::Lite;

any '/' => sub {
    my ($c) = @_;
    return $c->render('index.tt');
};

get '/user/status/:user_id' => sub {
    my ($c,$args) = @_;

    my $row = $c->api('User')->fetch($args);

    return $c->render('user_status.tt',$row->get_columns);
}
1;

モデルのテストコード

package t::Util;

#ここを追加
use MyApp;
sub import {
    my $caller = caller(0);

    MyApp->bootstrap;

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

t::UtilでAmon2のbootstrapを叩いて、モデルの個別テストファイルでは、bootstrapしないようにしています。

# t/User/fetch_user.tとかにしておく
use strict;
use warnings;
use utf8;
use t::Util;
use Test::More;
use MyApp::Container qw/api/;
use Test::Deep;
use Test::Deep::Matcher;

DBのセットアップとかsetup的なゴニョゴニョ

my $user_id = ...;

my $row = api('User')->fetch($user_id);

cmp_deeply ($row->get_columns, +{
    id   => is_number,
    name => is_string,
    age  => is_number,
    sex  => is_string,
});

DBを消したりなどのゴニョゴニョ

done_testing;

終わりに

個人的には、「特定のクラス配下にクラスを用意」するほうが多いです。「DBクラスを拡張」する場合は、むしろコントローラー(Dispatcher)にゴリゴリ書いちゃっていいんじゃないかなーという気がしています。

とりあえず、issmさんに紹介していただいたコードを、Modelの名前空間いじれるようにして汎用化するのが一番いいのではないでしょうか?w 私自身は、Containerが内部で実装されているWAFのKamuiを使っていたので、なんとなくContainerをつかなわないと落ち着かない病なので、Containerをあえてかましていますが(Containerさえ入れ込めれば、プロジェクト別に作ったクラスもコピペが用意!w)、ゼロベースで考えると、issmさんの方法がいい気がします。

あと、Amon2の要素だけを抽出して加工したサンプルコードにしようと思ったんですが、オレオレっぽさを出そうと思って、普段使っているモジュールもそのまま入れておきました。なんじゃこりゃってものもあるかもですが、まぁオレオレということで、自由にDisって頂ければと思いますーw

さて、皆さんは具体的にどう実装されているでしょうか。なんかダラダラ書いたので、誤字脱字も含め、どしどし、ツッコミお待ちしておりますー!

Perl update_at : 2013-01-06T13:25:05
hirobanex.netの更新情報の取得
 RSSリーダーで購読する   
blog comments powered by Disqus