読者です 読者をやめる 読者になる 読者になる

$shibayu36->blog;

プログラミングの話や自分の考えを色々と書いています。

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

【2016/03/04追記】以前まとめたこのMVACという名前の設計は既に古くなっており、今はこのようなアーキテクチャで設計していません。


 こんにちは。最近ははてなでMVACというアーキテクチャに則って開発をしているのですが、ようやく意味を理解できてきました。そこで今回は「Web Applicationを綺麗に設計するためのMVACという考え方」について、サンプルを交えながら説明していこうと思います。かなり長くなってしまったので、時間があるときにでもどうぞ。

MVACって?

 データソースやロジックを扱う「Model」、表示・出力を管理する「View」、複数のModelとControllerをつなぐApplication、ユーザのリクエストなどを受け取りViewやApplicationを制御する「Controller」の4つの要素を組み合わせてシステムを実装する方式。MVCをさらに抽象化した方式。id:secondlifeさんがデブサミ2009で発表しました。また、id:kazuk_iさんの資料も分かりやすいです。

MVCとは

 詳しい説明に入る前にまずはMVCの説明をしておきます。MVCとは、「ソフトウェアの設計モデルの一つで、処理の中核を担うModel、表示・出力を司るView、入力を受け取ってその内容に応じてViewとModelを制御するControllerの3要素の組み合わせでシステムを実装する方式」のことです。

 よく下のような図で説明されると思います。

 Mvcモデルについての解説は下の資料が参考になると思います。

MVCの問題点

 MVCではそれぞれが次のような役割を持っています。

Model
データのやり取りやロジック部分を担当する。オブジェクト層やサービス層に分類することも出来る。
View
Controllerから受け取ったデータから、実際の表示の生成を行う。
Controller
ユーザからのリクエストを受け取り、リクエストをハンドリングしたり、Modelをいくつか利用して処理を行う。またその時作成したデータをViewに受け渡す。

 ここで問題が一つあります。それはControllerが「リクエストをハンドリングする」という役割と、「Modelをいくつか利用して処理を行う」という、二つの違う役割を担っていることです。ここにおける「リクエストをハンドリングする」というのは、HTTP Methodの取り扱いや、リクエストパラメータの受け取り、リダイレクト処理などです。「Modelをいくつか利用して処理を行う」というのは実際の処理ですね。

 さて、このことがなぜ問題になるかというと、

  • 二つの役割がごっちゃになっているせいで、少しずつControllerが汚くなっていく
  • 二つの役割を行っているため、Controllerの単体テストが行いにくい!

というような問題があるからです。特にControllerの単体テストが行いにくいというのは、テストを書かない原因となり、バグを混入してしまう可能性が増えてしまいます。。

なぜMVAC?

 そこで登場したのがMVACです。MVACの場合、それぞれが次のような役割を持つと考えられます。

Model
データのやり取りやロジック部分を担当する。オブジェクト層やサービス層に分類することも出来る。
View
Controllerから受け取ったデータから、実際の表示の生成を行う。
Application
Controllerから受け取ったデータから、複数のModelを利用し、実際の処理を行う
Controller
ユーザからのリクエストを受け取り、リクエストをハンドリングして、Applicationにデータを渡す。

 上の役割を見ると、さきほど問題になっていたControllerが二つの違う役割を担っているという部分が解消されて、それぞれControllerとApplicationによって役割分担されています。これによって、Applicationでは実際の処理のテストを、Controllerではリクエストのハンドリングのテストを行えばよいので、テストがしやすくなることがわかります。

 図で見るとMVACは次のようなアーキテクチャになると思います。

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

 それではMVACでの処理の流れをサンプルを利用しながら説明していきます。サンプルはhttps://github.com/shiba-yu36/p5-Mvac-Sampleにあるので、githubかもしくはcloneしてきて適当に見てください。

 今回はControllerやそれらの繋ぎこみにMojoliciousを、ViewにXslateを、ModelのDB部分にSkinnyを使ってます。

 商品管理を行うサイトとしてサンプルを作成しています。その中で今回は商品のタイトルや画像などを指定して新規登録する処理に注目して説明していきます。下の画像で送信ボタンを押したときの処理です。


処理の流れ

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

  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が必要な処理を実行

 Controller内でApplicationに必要な処理を実行させます。
 サンプルではlib/Mvac/Sample/Controller/Products.pm*3のcreateメソッドの中ほどでvalidationと商品の保存を実行しています。

# 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が実際に処理されたデータをViewに渡します。
 サンプルではこの処理が少しわかりにくくなってます。処理されたデータを渡すのではなく、Applicationオブジェクトを渡すことで、テンプレートでもApplicationを使えるようにしています。ただし、createメソッドではredirectしているので、実際にはこのデータは使われていません。
 実際にはlib/Mvac/Sample/App.pm*5内のprepare_from_controllerメソッドで行っています。これはstashに共通の処理を書くのが面倒という理由からです。

# このメソッドを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しているので実際の処理はありません。他のアクションを見てもらえばわかると思います。


 と、以上のような流れで処理が実行されていきます。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クラスから用いるようにすれば、綺麗に書けますね。

まとめ

 非常に長くなりましたが、今回はMVACというアーキテクチャで綺麗にWeb Applicationを設計する方法についてまとめてみました。
 Model, View, Application, Controllerと分割して設計することで、綺麗にかけたり、テストをしやすくなったりすることや、さらにModelをオブジェクト層やサービス層に分離するやり方などを説明出来たと思います。
 もし何かおかしいところや突込みどころなどあれば@shiba_yu36まで教えてください! 

3/8追記

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