俺とModelとAPI

概要

APIという名前空間についていろいろな意見もあると思いますが、私はこんな風に実装していますよ、という一人言です。
私が説明するのが下手なせいか、今まで誰にも理解されなかったという俺流APIの使い方です。

実装例

はい、ドーン

package MyApp::Api::Msg;
use strict;
use warnings;

sub new { return bless { _msgs => [] }, $_[0]; }

sub get_msgs { @{ $_[0]->{_msgs} }  }

sub has_msgs { scalar $_[0]->get_msgs }

sub set_msg { 
    my ( $self, $new_msg ) = @_;
    $self->{_msgs} = [ $self->get_msgs, $self->_edit_preset_msg($new_msg) ];
}

sub _edit_preset_msg { return $_[1] } # override if you want

package MyApp::Api::DebugMsg;
use strict;
use warnings;
use base qw/MyApp::Api::Msg/;
use Time::HiRes ();

sub new {
    my $class = shift;
    my $self = $class->SUPER::new;
    $self->{_start} = [Time::HiRes::gettimeofday];
    return $self;
}

sub start { $_[0]->{_start} }

sub _edit_preset_msg {
    my ($self, $msg) = @_;

    my $now = _now();
    my $elapsed = Time::HiRes::tv_interval($self->start, [Time::HiRes::gettimeofday]);

    return "[$now][elapsed $elapsed] $msg";
}

### copied from Log::Minimal
sub _now {
    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) =
      localtime(time);

    my $time = sprintf(
        "%04d-%02d-%02dT%02d:%02d:%02d",
        $year + 1900,
        $mon + 1, $mday, $hour, $min, $sec
    );
}

package MyApp::Api::Base;
use strict;
use warnings;
#use MyApp::Api::Msg;
#use MyApp::Api::DebugMsg;
use IO::File;
use Class::Accessor::Lite 0.05 ( 
    ro => [qw/log_path/],
    rw => [qw/error_msg status_msg debug_msg/] );

sub new {
    my ($class, %args) = @_;
    
    bless {
        log_path   => $args{log_path},
        status_msg => MyApp::Api::Msg->new,
        error_msg  => MyApp::Api::Msg->new,
        debug_msg  => MyApp::Api::DebugMsg->new,
    }, $class;
}

sub set_status_msg { $_[0]->status_msg->set_msg( $_[1] ) }

sub set_error_msg { $_[0]->error_msg->set_msg( $_[1] ) }

sub set_debug_msg { $_[0]->debug_msg->set_msg( $_[1] ) }

sub DESTROY {
    my $self = shift;
    
    if ( $self->log_path and $self->debug_msg->has_msgs ) {

        my $fh = IO::File->new( $self->log_path, 'a');
        
        $fh->print( '-' x 10, "\n" );
        $fh->print($_) for map { "$_\n" } $self->debug_msg->get_msgs();
        $fh->print("\n");
        $fh->close;
    }
}

package MyApp::Api::Hoge;
use strict;
use warnings;
use base qw/MyApp::Api::Base/;

sub new {
    my ($class, %args) = @_;
    my $log_path = delete $args{log_path} || undef;
    my $self = $class->SUPER::new(log_path=>$log_path);
    return $self;
}

sub something {
    my $self = shift;

    $self->set_debug_msg('start something');
    $self->_another_func;
    $self->set_debug_msg('end something');
}

sub _another_func {
    my $self = shift;

    $self->set_debug_msg('start another_func');
    sleep(1);
    $self->set_debug_msg('end another_func');
}

package main;
use strict;
use warnings;
#use MyApp::Api::Hoge;

my $api = MyApp::Api::Hoge->new(log_path=>'api.hoge.log');
$api->something();

ログ

実行するとこんな感じでログがでる

cat api.hoge.log
----------
[2011-02-21T11:04:45][elapsed:0.000148] start something
[2011-02-21T11:04:45][elapsed:0.000217] start another_func
[2011-02-21T11:04:46][elapsed:1.001758] end another_func
[2011-02-21T11:04:46][elapsed:1.001824] end something
----------
[2011-02-21T11:12:44][elapsed 0.000154] start something
[2011-02-21T11:12:44][elapsed 0.000225] start another_func
[2011-02-21T11:12:45][elapsed 1.001222] end another_func
[2011-02-21T11:12:45][elapsed 1.001286] end something

APIとBusiness Model

APIは大きく分類すればMVCでいうところのBusiness Modelに含まれると思いますが、私は次の処理を記述する場合、実装するレイヤーを明確にしたい衝動に駆られてしまったのです。

  • 意図していない操作を行った場合は例外を送出するレイヤー(BusinessModel)
  • validationなど、入力間違いがあった場合にエラーメッセージを返すレイヤー(Api)
  • 処理時間にどれぐらい経過したのか、などのinfo, debugログを仕込むレイヤー(Api)

Apiを実行しても例外を送出しない事を約束したい

Try::Tinyやevalの記述はこのレイヤーのみで行われるべきだと考えてみました。
入力した文字がvalidation errorを起こした場合はerror_msgsにその旨を記しておきます。

複数人で開発していると、誰かが作ったモジュールを使用する事があると思います。
で、もし処理に失敗したら例外が起きるだろうと思ってtry,catchしてたら内部でもtry, catchしていたという罠に出会います。

分かりづらいし、バグの元になったりするので例外をキャッチするのはApiという名前空間だけにしたい。

error_msgsとstatus_msgs

よくある入力フォームではこんな感じで記述したい。

# MyApp::Web::Controller::User::Edit
sub complete {
	my ($self, $c) = @_;

	my $api = MyApp::Api::User::Edit->new;
	$api->edit($c->req->params);

	$c->forward('/user/edit', {error_msgs=>$api->get_error_msgs}) if $api->has_error_msgs;

	$c->flash->{status_msgs} = $api->get_status_msgs;
}

Web::Controller側でstatus_msgsとかerror_msgsを記述しても大差ないのですが、CLIから呼び出す事が
あるかもしれないのでこっちのほうが私は好きです。

処理に経過した時間を知りたい

私は仕事でプログラムを書いていますが、どちらかというと休日も趣味でプログラムを書いています。
そうするとRSSの処理をしたり、公開WebAPIへのリクエストを行ったり、バッチ処理を書いたりすることが多いです。

そしてこの処理はWeb::Controller, CLIともに共通化していて、
これらの処理がどれぐらい時間がかかっているかを計測する手段が必要なので処理時間を計測してログを取得するようにしています。

ただし、FCGIなど複数のプロセスが同時にログを書きこむとわけがわからなくなるので一つ一つの処理の単位をApi::Hogeインスタンス化されてから、そのインスタンスが破棄されるまで、をひとつの単位としています。

そうすると結果的にログの記述はすべてApiというレイヤーに限定されました。

トランザクションはどこに記述する問題

DB操作を行う場合、次のような記述をします。

package MyApp::Api::Shopping; 
sub buy {
	my ( $self, $user, $item ) = @_;

	if ( $user->can_buy($item) ) {
		$user->buy($item);
	}
	...
}

なんとなく、トランザクションApiに書いてしまう予感がしますが、最終的にトランザクションはしない、という結論です。
この場合はversion number 楽観ロックを使うようにします。

http://d.hatena.ne.jp/okamuuu/20100725/1280064341

ということで、こういう感じになる

package MyApp::Api::Shopping; 
sub buy {
	my ( $self, $user, $item ) = @_;

	if ( $user->can_buy($item) ) {
		$user->buy($item) or $api->set_error_msgs('アイテムの購入ができませんでした。');
	}
	...
}

Web::ControllerからBusiness Modelを直接呼んではいけないのか?

否。show, listといったDBの書き込みが発生しない画面でAPIというレイヤーをわざわざつくると面倒くさい時があります。
むしろDBでの書込みが発生する処理(誰が、いつ、何をした、行うとしたのか、どれぐらい時間を要しているのか)といった情報が欲しいので
そういう場合に積極的にAPIというレイヤーを使いたい、という考え方をしています。

まあユーザーがアイテム買ったのにアイテム増えてない!といわれてもこういったログがあれば安心です。

本当にそうやって実装しているのか?

実は自分家でアプリ作るときしかそうやってないです。冒頭に書いたとおり、だれも理解してくれないので会社では実装できません。
だれも賛成しないので、そんなにうまいやり方ではないのかもしれません。おしまい。