こんにちは。最近ははてなでMVACというアーキテクチャに則って開発をしているのですが、ようやく意味を理解できてきました。そこで今回は﹁Web Applicationを綺麗に設計するためのMVACという考え方﹂について、サンプルを交えながら説明していこうと思います。かなり長くなってしまったので、時間があるときにでもどうぞ。
MVACって?
![](https://cdn.profile-image.st-hatena.com/users/secondlife/profile.png)
![](https://cdn.profile-image.st-hatena.com/users/kazuk_i/profile.png)
MVCとは
MVCの問題点
なぜMVAC?
![f:id:shiba_yu36:20110303122726g:image](https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiba_yu36/20110303/20110303122726.gif)
MVACアーキテクチャでのサンプルによる処理の流れ
![f:id:shiba_yu36:20110303122727p:image](https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiba_yu36/20110303/20110303122727.png)
処理の流れ
主に処理の流れは以下のようになります。これらをそれぞれ見ていきます。
- Controllerがリクエストを受け取り、ハンドリングする
- Controllerが必要なデータをApplicationに渡す
- Applicationが必要な処理を実行
- Application内でModelを利用し、処理を完了させる
- ControllerがViewにデータを渡す
- Controllerが表示するViewのハンドリングを行う
- Viewが実際の表示を作成する
Controllerがリクエストを受け取り、ハンドリングする
まずは送信ボタンを押したすぐ後から見ていきます。
リクエストからアクセス権をチェックしたり、HTTP Methodのチェック等を行います。
Sampleではlib/Mvac/Sample/Controller/Products.pm*1のcreateメソッドの上側でHTTP Methodのチェックを行っています。
# require post return $self->render(template => 'products/new_product') if ($self->req->method ne 'POST');
Controllerが必要なデータをApplicationに渡す
リクエストパラメータで受け取った値を必要に応じてApplicationのクラスに渡します。
サンプルではlib/Mvac/Sample/Controller/Products.pm*2のcreateメソッドの中ほどで以下のように渡しています。
my $app = app_class('Products')->new; $app->prepare_from_controller($self); $app->title($req->param('title')); $app->description($req->param('description')); $app->type($req->param('type')); $app->small_image($req->upload('small_image')); $app->large_image($req->upload('large_image'));
Applicationが必要な処理を実行
# validate unless ($app->check_create_input) { return $self->render(template => 'products/new_product'); } # save $app->save_product;さらにlib/Mvac/Sample/App/Products.pm*4ではこの処理の実装を行っています。実際に渡されたパラメータからvalidateを行う、商品の保存︵DBへの保存、画像のアップロードなど︶を行っています。内部でModelを利用していることも見れます。
sub check_create_input { my $self = shift; my $title = $self->title; my $description = $self->description; my $type = $self->type; my $small_image = $self->small_image; my $large_image = $self->large_image; my $result = $self->_check_req_param; $self->_check_valid_image('small_image', $result); $self->_check_valid_image('large_image', $result); $self->form($result); return !$result->has_error; }
sub save_product { my ($self) = @_; my $upload_dir = $self->config->{photo_upload_dir}; my $model = $self->model; my $title = $self->title; my $description = $self->description; my $type = $self->type; my $small_image = $self->small_image; my $large_image = $self->large_image; # save small image my $small_image_name = $self->_upload_image($small_image, $upload_dir . 'small/'); # save large image my $large_image_name = $self->_upload_image($large_image, $upload_dir . 'large/'); # order my $count = $model->count('products', 'id', {type => $type}); my $order = $count + 1; # save product my $upload_path = $self->config->{photo_upload_path}; my $product = $model->insert('products', { title => $title, description => $description, type => $type, order_num => $order, small_image_url => $upload_path . 'small/' . $small_image_name, large_image_url => $upload_path . 'large/' . $large_image_name, }); }
ControllerがViewにデータを渡す
# このメソッドをControllerから呼んでいる sub prepare_from_controller { my ($self, $c) = @_; $self->config($c->stash('config')); $self->model($c->app->model); $c->stash(app => $self); # ここでセットされる return $self; }
Controllerが表示するViewのハンドリングを行う
redirect処理や表示するtemplateを決める部分です。
サンプルでは単純にリダイレクトさせています。
$self->redirect_to('products');
Viewが実際の表示を作成する
と、以上のような流れで処理が実行されていきます。ApplicationクラスをControllerとModelとのつなぎに利用することで、﹁リクエストをハンドリングする﹂部分と、﹁Modelをいくつか利用して処理を行う﹂部分とが分離され、見やすくなっていることがわかりますね。
MVACアーキテクチャでのサンプルによるテスタビリティ
MVACを利用する利点として、テストがしやすくなるということを挙げました。これについても説明します。テストは特にModelの部分とApplicationの部分を中心に行っていくのがいいと思います。今回はApplicationクラスを利用して抽象化しているので、リクエストの処理の部分を考えずに、テストを行うことが出来ます。
サンプルではt/app/products.t*6が分かりやすいと思います。ここにある_delete_productのテストでは商品を仮に作っておいて、そのパラメータをApplicationに渡すだけでApplicationのクラスのテストが行えます。もしApplicationのクラスがなかった場合はこのようなテストをするときにすら、リクエストのオブジェクトを生成しないといけないので大変になることがわかると思います。
sub _delete_product : Test(3) { my ($self) = @_; my $p1 = create_product; my $p2 = create_product; my $app = app_class('Products')->new; $app->model(Mvac::Sample::Model->new); $app->id($p1->id); $app->delete_product; my $orgs = Mvac::Sample::Model->products_from_type('original')->all; is @$orgs, 1; is $orgs->[0]->id, $p2->id; is $orgs->[0]->order_num, 1; delete_products; }
その他備考など
今回はそこまで複雑なことをしなかったので、Model部分はオブジェクト層であるlib/Mvac/Sample/Model.pm*7やlib/Mvac/Sample/Model/Row/Product.pm*8などしかつくりませんでした。実際はModelをDB接続など一つのものと対応するオブジェクト層と、たくさんのものを扱うロジックであるサービス層に分けるといいと思います。
例えば「何回か来ているユーザに、その行動から登録した商品のどれかを推薦する」とかをしたい場合、いろんな場所(コントローラ)でそのロジックを使いたくなります。その時はlib/Mvac/Sample/Service/Recommend.pmとかを作って、その中でユーザの情報やいくつかの商品を使って何かしらの推薦を出すようなModelを作成します。そしてRecommendをその機能が必要なApplicationクラスから用いるようにすれば、綺麗に書けますね。
まとめ
3/8追記
今回いただいた意見などを受けて、補足のためのエントリを書きました。この記事だけだと誤解を生む場合があるので、そちらも御覧ください。
補足 - Web Applicationをきれいに設計するためのMVACという考え方 - $shibayu36->blog;
*1:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/lib/Mvac/Sample/Controller/Products.pm
*2:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/lib/Mvac/Sample/Controller/Products.pm
*3:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/lib/Mvac/Sample/Controller/Products.pm
*4:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/lib/Mvac/Sample/App/Products.pm
*5:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/lib/Mvac/Sample/App.pm
*6:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/t/app/products.t
*7:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/lib/Mvac/Sample/Model.pm
*8:https://github.com/shiba-yu36/p5-Mvac-Sample/blob/master/lib/Mvac/Sample/Model/Row/Product.pm