05日目: モデル、ビュー、コントローラ

昨日までで DBIx::Class を使用したデータベースのつかい方、またそのクラスを Ark から使用する方法を学びました。 そしてそれを用いて Jobeet のデータベーススキーマを記述したりテーブルを作成したり初期データをデータベースに投入したりしました。

今日は昨日作ったデータベースを元に Job 周りのモジュールを作ってきます。これらは複数のファイルから構成され、以下の機能を持ちます:

MVC アーキテクチャ

Web 開発の分野では近年コーディングのための最適解として認識されているのは MVC デザインパターンです。 手短に言えば、MVC デザインパターンはコードの性質ごとに体系化する方法を定義しています。このパターンは3つのレイヤーに分けられます。

MVC

MVC パターンをはじめたばかりの初心者によくある間違いとして、コントローラにロジックを書いてしまう、と言う物があります。コントローラはあくまで HTTPからの入力 をうけとって何かをするものですので、コントローラにロジックを書いてしまうとそのロジックは HTTP 経由でしか実行できなくなってしまいます。ロジックはすべてモデルに定義し再利用性のたかいコードを書くよう気をつけましょう。

ビューの準備

レンダリングを担当する View クラスは Jobeet::View:: 以下に定義します。 テンプレートや JSON データなどほとんどのデータ形式の場合 Ark 自体が View クラスを用意しているので、アプリケーションではそれを継承するクラスを用意するだけでビューの機能を使用することが出来るようになっています。

とりあえずテンプレートに対応する View を作成してみましょう。Ark ではデフォルトでいくつかのテンプレートエンジンに対応していますが、このチュートリアルでは Text::Microtemplate のビューを使用します。 以下のコマンドを実行してください。

$ script/dev/skeleton.pl view MT

これで Ark::View::MT を継承した Jobeet::View::MT クラスが自動的に作成されます。

そしてコントローラ内で

$c->forward( $c->view('MT') );

としてこの作成したビューに処理を配送することでテンプレートのレンダリングが行われます。Jobeet::Controller に以下の定義を付け加えてください。

sub end :Private {
    my ($self, $c) = @_;

    unless ($c->res->body or $c->res->status =~ /^3\d\d/) {
        $c->forward( $c->view('MT') );
    }
}

end アクションはコントローラの実行の最後に実行される特別なアクションで、これはレスポンスがまだ定義されていない場合、MT ビューに処理を配送すると言う処理を行っています。 そのほかのコントローラの詳細については明日学習しましょう。

Job ロジックの作成

Ark モデル

ロジックはモデルに定義するのでしたね。昨日あいまいにしてしまった Ark モデルの仕組みを先に学んでおきましょう。

Ark が提供するのはモデル自体ではなく Perl モジュールの入れ物です。 昨日 Jobeet::Schema とその各 ResultSet クラスを Jobeet::Models に登録したのを覚えているでしょうか? Ark では普通の Perl のモジュールをモデルとして使用します。 こうすることで膨大な数の CPAN モジュールを再利用できるという利点があります。

基本的な使い方

アプリケーションごとにモデルの入れ物を定義します。定義は昨日したような:

package Jobeet::Models;
use strict;
use warnings;
use Ark::Models '-base';

1;

というような use Ark::Models '-base'; を書いたクラスを一つ用意します。これがこのアプリケーションのモデルクラスとなります。ここに

register モデル名 => sub {
    初期化処理、最後にこのモデルのオブジェクトを返す
};

と言う書式でモデルを登録します。

この登録したモデルを使用するときは、

use Jobeet::Models;

としてから

my $obj = models('Schema');
my $obj = models->get('Schema');
my $obj = Jobeet::Schema->get('Schema');

などとすると引数で渡した名前(ここではSchema)のモデルクラスを取得できます。

Ark モデルを使用する利点

Perl モジュールをモデルとして使用するなら、直接そのモジュールを Ark から使用すればいいのでは? と思うかもしれません。 しかし Ark モデルは以下の機能を提供します。

遅延ロード

register でモデルを登録しても実際には初期化処理は走りません。 Ark モデルではモデルを実際に使用するときに初めて初期化処理がされます。したがって、必要のない機能は register してあっても使用されず、効率的にメモリを使用することが出来、またその分高速に動作します。

configとの連動

Ark::Models にはデフォルトで定義されている二つのモデル(home, conf)があります。これらはそれぞれ:

を表します。昨日の Schema 定義の中で

$self->get('conf');

と言う書式で設定ファイルを参照していたのはこの機能をつかっていたのでした。

依存性の解決

初期化処理の中で get メソッドを使用してほかのモデルに依存することが出来ます。昨日の Schema の例を見てみましょう。

register Schema => sub {
    my $self = shift;

    my $conf = $self->get('conf')->{database};
    Jobeet::Schema->connect(@{ $conf });
};

register 'Schema::Job' => sub {
    my $self = shift;
    $self->get('Schema')->resultset('Job');
};

この定義では Schema::Job の中では get('Schema') していますし、Schema の中では get('conf') していますね。 これらは get するときに初期化されると最初に言いました。つまりどこで models('Schema::Job') とした瞬間に、

という処理で Schema::User のオブジェクトを取得することが出来ます。もちろんすでに初期化されたオブジェクトはキャッシュされるので初期化処理が何回も行われてしまうと言うことはありません。

Arkモデルを使用すると、このようにことで必要なものだけ初期化したり依存性のある複雑なモジュール間の連携を行うことができます。

Job ロジック

最初に上げた Job 周りのロジックをもう一度見てみましょう。

これをみるかぎり、ほとんどデータベース処理と各ページの機能は一致しています。 したがってJobロジックは定義せず、昨日定義した ORM クラスさえあれば事足りそうです。Job のロジックとしては Schema::Job モデルを使用していきましょう。

コントローラ

コントローラはユーザーのリクエストを受け(HTTPリクエスト)View や Model に処理を配送するものだと言いました。 つまり、Ark では URL はコントローラで作成します。

Job 用のコントローラ(URL)を作成しましょう。ただし、まだフォームについて学習していないので今日はジョブの一覧ページだけを作成しましょう。

/job/

にアクセスすると job 一覧が表示されるようにしてみましょう。Jobeet::Controller::Job と言う名前で以下のような Job コントローラを作成しましょう。

package Jobeet::Controller::Job;
use Ark 'Controller';

use Jobeet::Models;

sub index :Path {
    my ($self, $c) = @_;

    $c->stash->{jobs} = models('Schema::Job');
}

1;

コントローラとURLのマッピング(ルーティング)は明日詳しく学習しますが、このコントローラの index 関数は /job/ にリクエストが来たとき実行されます。 そしてここでは stash->{jobs} に Job 一覧を格納しています。 stash とはグローバルな変数の入れ物であり、コントローラとビューの間で変数を共有するのに使われます。つまりこの場合 jobs と言うデータをビューで使えるようにしていると解釈してもらってかまいません。

テンプレートの定義

さて、あとはテンプレートを定義すれば Job の一覧が表示されるはずです。

まず、モックアップをじっと見てみると各ページのほとんどが同じ部品であることに気づくでしょう。 Perl や HTML であろうとなかろうと、コードの重複は悪いことです。だからコードが重複している View 要素を抑える方法が必要となります。

この問題を解決する1つの方法として各テンプレートでヘッダーとフッターを定義する方法があります:

header footer

しかしこの場合ヘッダーやフッターは有効な HTML を含んでいません。よい方法であることは違いありません。車輪の再発明をする代わりにこの問題を解決するため別のデザインパターンを使うことにします。 それはテンプレートの継承です。

ベースとなるテンプレートを作成し、それを継承することで実際のページを作成します。以下のようなベーステンプレートを root/common/base.mt として定義しましょう。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  <head>
    <title>Jobeet - Your best job board</title>
    <link rel="shortcut icon" href="/favicon.ico" />
    <? block javascripts => '' ?>
    <? block stylesheets => '' ?>
  </head>
  <body>
    <div id="container">
      <div id="header">
        <div class="content">
          <h1><a href="<?= $c->uri_for('/') ?>">
            <img src="/images/logo.jpg" alt="Jobeet Job Board" />
          </a></h1>

          <div id="sub_header">
            <div class="post">
              <h2>Ask for people</h2>
              <div>
                <a href="<?= $c->uri_for('/job/new') ?>">Post a Job</a>
              </div>
            </div>

            <div class="search">
              <h2>Ask for a job</h2>
              <form action="" method="get">
                <input type="text" name="keywords"
                  id="search_keywords" />
                <input type="submit" value="search" />
                <div class="help">
                  Enter some keywords (city, country, position, ...)
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>

      <div id="content">
        <div class="content">
? block content => '';
        </div>
      </div>

      <div id="footer">
        <div class="content">
          <span class="symfony">
            <img src="/images/jobeet-mini.png" />
            powered by Ark
          </span>
          <ul>
            <li><a href="">About Jobeet</a></li>
            <li class="feed"><a href="">Full feed</a></li>
            <li><a href="">Jobeet API</a></li>
            <li class="last"><a href="">Affiliates</a></li>
          </ul>
        </div>
      </div>
    </div>
  </body>
</html>

<? block ... ?> と言う定義に注目してください。Ark のテンプレートではこのようにベーステンプレートでいろいろなブロックを定義しておき、それを継承した子テンプレートでそのブロックを上書きしていきます。

スタイルシート、画像、JavaScript

このチュートリアルの目的は Web デザインではないので、Jobeet で必要なすべてのアセットはすでに用意されています: 画像ファイルをダウンロードして root/images/ ディレクトリに設置します; スタイルシートファイルをダウンロードしてweb/css/ディレクトリに設置します。

job テンプレート

それでは /job/ 用のテンプレートを定義していきましょう。

root/job/index.mt として以下のようなテンプレートを書いてみましょう

? extends 'common/base';

? block stylesheets => sub {
<link rel="stylesheet" type="text/css" href="<?= $c->uri_for('/css/main.css') ?>" />
<link rel="stylesheet" type="text/css" href="<?= $c->uri_for('/css/jobs.css') ?>" />
? }

? block content => sub {
<div id="jobs">
  <table class="jobs">
? my $i = 0;
? for my $job ($c->stash->{jobs}->all) {
? $i++;
      <tr class="<?= $i % 2 == 0 ? 'even' : 'odd' ?>">
        <td class="location"><?= $job->location ?></td>
        <td class="position">
          <a href="<?= $c->uri_for('/job', $job->id) ?>">
            <?= $job->position ?>
          </a>
        </td>
        <td class="company"><?= $job->company ?></td>
      </tr>
? } # endfor
</table>
</div>
? } # endblock content

このテンプレートは extends 'common/base' で先ほど作成したベーステンプレートを継承し、そしてその後、stylesheets、content と言う二つのブロックを上書きしています。

content ブロックの中では先ほどコントローラからわたされた $c->stash->{jobs} オブジェクトを使用して Job の一覧を出力しています。

ここまででジョブの一覧ページはうまく動くはずです。以下のコマンドで開発サーバーを立ち上げて見てください。

$ plackup -r dev.psgi

そしてブラウザーで http://127.0.0.1:5000/job/ にアクセスしてみてください。

homepage

このような画面が表示されましたか? 昨日データベースに投入したデフォルトのJobデータが表示されましたね。 Jobの編集画面などは Ark のフォームクラスを学習した後実装していきましょう。

また明日

本日は MVC パターンと、Ark におけるそれらの具体的な役割について学習しました。 明日はコントローラとURLのマッピングについての詳細について学びます。