$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Web Applicationを綺麗に設計するためのMVACという考え方


2016/03/04MVAC


 MVACWeb ApplicationMVAC


MVACって?


 ModelViewModelControllerApplicationViewApplicationController4MVCid:secondlife2009id:kazuk_i

MVCとは


 MVCMVCModelViewViewModelController3

 

 Mvc

MVCの問題点


 MVC


Model



View

Controller

Controller

ModelView


 ControllerModelHTTP MethodModel

 


Controller

Controller


Controller


なぜMVAC?


 MVACMVAC


Model



View

Controller

Application

ControllerModel

Controller

Application


 ControllerControllerApplicationApplicationController

 MVAC


MVACアーキテクチャでのサンプルによる処理の流れ


 MVAChttps://github.com/shiba-yu36/p5-Mvac-Samplegithubclone

 ControllerMojoliciousViewXslateModelDBSkinny使

 


処理の流れ

 主に処理の流れは以下のようになります。これらをそれぞれ見ていきます。

  1. Controllerがリクエストを受け取り、ハンドリングする
  2. Controllerが必要なデータをApplicationに渡す
  3. Applicationが必要な処理を実行
    1. Application内でModelを利用し、処理を完了させる
  4. ControllerがViewにデータを渡す
  5. Controllerが表示するViewのハンドリングを行う
  6. 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が必要な処理を実行

 ControllerApplication
 lib/Mvac/Sample/Controller/Products.pm*3createvalidation
# validate
unless ($app->check_create_input) {
    return $self->render(template => 'products/new_product');
}

# save
$app->save_product;

 lib/Mvac/Sample/App/Products.pm*4validateDBModel
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にデータを渡す

 ControllerView
 ApplicationApplication使createredirect使
 lib/Mvac/Sample/App.pm*5prepare_from_controllerstash
# このメソッドを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が実際の表示を作成する

 View 
 redirect


 ApplicationControllerModelModel


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クラスから用いるようにすれば、綺麗に書けますね。

まとめ


 MVACWeb Application
 Model, View, Application, ControllerModel
 @shiba_yu36 


3/8追記

 今回いただいた意見などを受けて、補足のためのエントリを書きました。この記事だけだと誤解を生む場合があるので、そちらも御覧ください。
補足 - Web Applicationをきれいに設計するためのMVACという考え方 - $shibayu36->blog;