モダンPerlの世界へようこそ

第8回Reaction:CatalystをもっとDRYに

アプリケーションの枠組みを越えた再利用

前回は、Catalyst 5.7で登場したチェーンドアクションを利用して適切なベースコントローラをつくれば、CRUDのような定型処理は再利用できるようになる、という話をしました。これはコンテントマネジメントシステム(CMS)のように同じようなインタフェースを持つ画面が多いシステムをつくるときには特に効果的なのですが、その再利用を、ひとつのアプリケーション内だけで完結させてしまうのはもったいない話。自社でつくるアプリケーションにはどんどん使い回していきたいものですし、コピー&ペーストを避けるためには、なんらかの名前空間上にその共通コードをまとめていく必要があります。

もちろんそのコードが小さく、十分に一般化できるものなら、Catalyst、あるいはCatalystXという名前空間に入れてもかまいませんが、コントローラの部品だけでなく、モデルやビューまで巻き込んで独自路線を突き進むのであれば、別の名前空間を用意したほうが賢明です。

今回はそのような例のひとつとして、mstことマット・トラウト(Matt S. Trout)氏を中心に開発が続けられているReactionというベタープラクティス集を取り上げてみます。

Reactionは、当時のデファクトスタンダードであったCatalyst+DBIx::ClassTemplate::Toolkitという組み合わせにおそらくはじめてMooseを持ち込んだ事例であり(CPANに最初のプレビュー版がリリースされた2006年5月当時、Mooseはまだ誕生から2ヶ月しかたっていませんでした⁠⁠、そのコードや考え方は、のちのCatalystの世界に大きな影響を与えてきました。

Reactionはかなり大きなコードなので全貌を把握するのは大変ですが、ここでは付属のテストアプリケーションやチュートリアルを参考に、大事なポイントをいくつか紹介していきます。

環境を構築する

ReactionはCPANに上がっていますので、単に使いたいだけであれば、いつもの手順でCPANからインストールできます[1]⁠。

ただし、今回はReactionに付属しているテストファイルを利用したいので、CPANからtarballをダウンロードして適当なディレクトリに解凍することにしましょう(このような場合は、CPANシェルを起動して「look Reaction」を実行するか、CPANPLUSを使っているのであればシェル(コマンドライン)から直接「cpanp -z Reaction」するのが手軽です。もちろんhttp://code2.0beta.co.uk/reaction/svn/以下のリポジトリから最新版をチェックアウト/エクスポートしてもかまいません⁠⁠。

tarballの解凍が済んだら、おなじみの「perl Makefile.PL && make test」を実行してください(これでテストとともにサンプルアプリケーションの環境設定が行われます⁠⁠。それが済んだら、下記のコマンドを実行してサーバを起動しましょう。環境によってはいくつか警告が表示されますが、深刻なエラー以外はひとまず無視していただいて結構です。ブラウザで http://localhost:3000/ を開くと、Reactionベースのアプリケーションを試すことができます。

> perl script/componentui_server.pl

ルートコントローラをのぞいてみよう

動作を確認したところで、実際のコードを見てみましょう。まずはエディタでlib/ComponentUI/Controller/Root.pmを開いてみてください。見慣れないもの、見かけないもの、それぞれにありますが、順不同で説明していきます。

まずは、前回やり方を紹介したように、ここでもすべてのチェーンの起点となるアクションが用意されています(Reactionではbaseという名前が使われています⁠⁠。push_viewportというのは見慣れませんが、いまはアクションごとにテンプレートの部品を順次登録しているものと思っておいてください(baseアクションではサイトのレイアウトの大枠を、rootアクションではindexというより具体的な部品を登録しています⁠⁠。

sub base :Chained('/') :PathPart('') :CaptureArgs(0) {
  my ($self, $c) = @_;
  $self->push_viewport(SiteLayout,
    title => 'ComponentUI test title',
    static_base_uri => "${\$c->uri_for('/static')}",
    meta_info => {
      http_header => {
        'Content-Type' => 'text/html;charset=utf-8',
      },
    },
  );
}

sub root :Chained('base') :PathPart('') :Args(0) {
  my ($self, $c) = @_;
  $self->push_viewport(ViewPort, layout => 'index');
}

push_viewportのなかではSiteLayoutやViewPortという裸のワードが使われていますが、これを実現しているのがaliasedというプラグマ。細かな使い方はPODを見ていただくとして、長い、決まり切ったパッケージ名が頻出する場合はこのプラグマを使うと短くすっきり書けるようになります。

use aliased 'Reaction::UI::ViewPort';
use aliased 'Reaction::UI::ViewPort::SiteLayout';

また、コードは転載しませんが、このルートコントローラでは静的ファイルやエラーの処理も行っています(エラー処理についてはベースクラスの方にもあるので省略可能です⁠⁠。よく見かけるRenderViewを使ったendアクションの姿もありませんが、これはベースコントローラのほうで似たような処理を行っているためです(ルートのベースコントローラに登録されているbeginでビュー(ビューポート)まわりの基本設定を行い、そこから各コントローラのチェーンドアクションなどでURLごとの処理をし、ふたたびルートのendで登録済みのビューポートを合成する、というのがReactionの基本的な流れになります⁠⁠。

スキンとレイアウト

Catalystでは伝統的にroot以下にテンプレートなどを置いていますが、Reactionではこれらはshare以下に用意します(こうしておくと、CPANからインストールしたときにデフォルトのテンプレートなどもあわせてインストールできます。詳しくはFile::ShareDirのPODをご覧ください⁠⁠。今回のサンプルアプリケーションの場合は、share/skin/componentui以下にテンプレートやCSSなどが用意されています。

試しにshare/skin/componentui/layout/site_layout.ttを開いてみましょう。PODに似た見慣れないディレクティブが出てきますが、ざっと説明しますと、

=extends NEXT

これは親テンプレート(この場合はshare/skin/default/layout/site_layout.ttを継承している、という意味。ちなみにデフォルト(default)のテンプレートはさらにベース(base)site_layout.ttを継承しています。

=for layout head_style

  <link rel="stylesheet" type="text/css"
    href="[% static_base %]/componentui-basic.css" />

この「=for layout head_style」というのは、以下のかたまりは[% head_style %]で呼び出せるテンプレートの部品ですよ、という意味(この呼び出しはベーステンプレートのsite_layout.ttで行われています⁠⁠。

=for layout inner
<!-- main content start -->
[% call_next %]
<!-- main content end -->

[% call_next %]は特殊で、次の(たいていは次のアクションで定義されている)ビューポートを呼ぶ、という意味。先ほどのrootアクションの例でいうと、⁠layout => 'index'」で指定されているshare/skin/componentui/layout/index.ttの中身を読み込む、ということになります。そちらのファイルも開いてみましょう。

=for layout widget

<p>I hate programming.</p>

=cut

widgetというのは、それぞれのテンプレートで最初に呼び出される部品の名前です。

テンプレートのカスタマイズはウィジェットで

せっかくなので少しテンプレートに手を加えてみましょう。静的な修正についてはテンプレートそのものを直せばよいのですが、動的に変更したい部分はそれぞれのテンプレートに対応するウィジェットというモジュールを用意して修正していくのがReactionの流儀。このindex.ttの場合はlib/ComponentUI/View/Site/Widget/Index.pmに空のウィジェットが用意されていますので、それをこのように書き換えてください。

package ComponentUI::View::Site::Widget::Index;

use Reaction::UI::WidgetClass;
use DateTime;

use namespace::clean -except => [ qw(meta) ];

after fragment widget {
    arg template_name => $_{viewport}->layout;
};

implements fragment render_time {
    arg time => sub { DateTime->now };
};

__PACKAGE__->meta->make_immutable;

1;

share/skin/componentui/layout/index.ttのほうはこのように修正します。

=for layout widget

<p>I hate programming.</p>
<p>[% template_name %]</p>
<p>[% render_time %]<p>

=for layout render_time

[% time %]

=cut

これでサーバを再起動して http://localhost:3000/ にアクセスすると、rootアクションで指定されていた「layout => 'index'」の値と「DateTime->now」の値がそれぞれ表示されるようになりました。また、ウィジェットモジュールのなかではMooseのafterモディファイアを利用して、すでに登録されているwidgetに対する処理を上書きしないようにしています(このように必要な部分に限って修正していけるのがReactionのウィジェットの利点といえます⁠⁠。

ベースコントローラがよくできていれば、あとは設定を書くだけ

テンプレートまわりはそのくらいにして、ふたたびコントローラに戻ってCRUDまわりの処理を見ていきましょう。

lib/ComponentUI/Controller/TestModel/Foo.pmを開いてください。コードは掲載しませんが、Reaction::UI::Controller::Collection::CRUDを継承しているほかは、ほとんど設定しかありません(_build_action_viewport_argsというメソッドもありますが、これも実際にはMooseのビルダーメソッドですから設定の一部といってよいでしょう)。ベースコントローラの抽象化が進むと、このように実際のアプリケーションでは設定だけ書けばおしまい、ということがまま起こります。

Foo.pmではかなり長めの設定が書かれていましたが、もちろんこのような設定のほとんどには初期値が用意されています。lib/ComponentUI/Controller/TestModel/Bar.pmのほうには必要最小限の設定しかされていません。起点となるアクションと、CRUDの対象となるモデル、コレクション(この場合はそれぞれデータベース名とテーブル名)だけ指定すれば、あとはよきにはからってくれることがわかります。

package ComponentUI::Controller::TestModel::Bar;

use base 'Reaction::UI::Controller::Collection::CRUD';
use Reaction::Class;

__PACKAGE__->config(
  model_name => 'TestModel',
  collection_name => 'Bar',
  action => {
    base => { Chained => '/base', PathPart => 'testmodel/bar' },
  },
);

1;

3つのモデル

続いては、model_nameで指定されていたlib/ComponentUI/Model/TestModel.pmを見てみましょう。これもほとんど定義と設定だけのモジュールですが、Catalyst::Model::Reaction::InterfaceModel::DBICというインタフェースモデルを継承しているだけでなく、どうやらComponentUI::TestModelという別のインタフェースモデルも利用しているらしいことが設定からわかります。

__PACKAGE__->config
  (
   im_class => 'ComponentUI::TestModel',
   db_dsn   => 'dbi:SQLite:t/var/reaction_test_withdb.db',
  );

たしかにlib/ComponentUIディレクトリの下にはTestModelといういかにもそれっぽい名前のディレクトリがありますから、これがDBIx::Classのスキーマなのだろうとあたりをつけて、lib/ComponentUI/TestModel.pmを開いてみると、肩すかし。今度はRTest::TestDBというスキーマクラスを利用していることがわかります。

my $reflector = Reaction::InterfaceModel::Reflector::DBIC->new;

$reflector->reflect_schema(
  model_class  => __PACKAGE__,
  schema_class => 'RTest::TestDB',
  sources => [
    qw/Foo Baz/,
    [ Bar => {attributes => [[-exclude => 'avatar']] } ], ## for now....
  ],
);

lib/RTestのようなディレクトリはありませんが、テストディレクトリのなかにあるt/lib/RTest/TestDB.pmを見ると、ようやくDBIx::Classのスキーマにたどりつきました。t/lib/RTest/TestDB/ディレクトリ以下にはいつも通りテーブル(コレクション)を定義しているモジュールが見つかります。

もっとも、テーブルを定義しているモジュールのなかには冗長なMooseのアトリビュート宣言があるため、一概に「いつも通り」とはいえないのですが、このようにデータベースまわりにいくつもモデルが用意されているのはなぜなのでしょうか。

インタフェースモデルの考え方

この問いにはいろいろな答え方がありそうですが、Reaction::Manual::Introの説明をかみ砕いていうと、第一の目的はO/Rマッパなどがモデリングしているドメインレベルのモデルからアプリケーションのビジネスロジックを切り離して再利用性を高めること、第二の目的は、⁠ウェブインタフェースとは直接関係のない)ビジネスロジックを(Catalystのコントローラではなく)モデルにまとめること、となります。

そう言われてもあまりピンと来ない方もいるかもしれませんが、伝統的なCatalyst::Model::DBIC::Schemaをモデルに使うと、概してCatalystのモデルのほうには設定しか書かず、アプリケーション固有のビジネスロジックは、Catalystのコントローラにベタ書きするか、次のメジャーバージョンで削除されるDBIx::Class::ResultSetManagerなどを利用して、スキーマ/リザルトセットクラスのほうに固有のメソッドを用意することになりがちでした(実際、Catalyst::Manualでもそのようなやり方になっています⁠⁠。

ただし、リザルトセットなどにビジネスロジックを書いてしまうと、認証やセッションまわりのように比較的使い回しのきくスキーマをほかのアプリケーションに再利用しようとしたときに問題が起こりがちですし、ドメインモデルの再利用性を重視してロジックをCatalystのコントローラに書いてしまうと、今度はCLIなどでそのモデルを利用するときにCLI側でも同じようなロジックをコピー&ペーストしてしまうことになってしまいます。

実際問題として、Catalystのモデルは普通に作ればCatalystのコンテキストとは無縁に再利用できてしまうものなのですが、場合によってはCatalyst向けの処理(設定ファイルのパス処理など)が入ってしまうこともあるので、Reactionでは明示的にドメインモデル(O/Rマッパ)とアプリケーションをつなぐインタフェース層を用意して、O/Rマッパ(スキーマ)のレベルでも、スキーマをラップするインタフェースのレベルでも、あるいはアプリケーションとインタフェースをつなぐCatalystモデルのレベルでも、ある種の再利用性を高められるようにしてあるわけです[2]⁠。

今回見ているComponentUIというサンプルアプリケーションの場合、インタフェースモデルに明示的にメソッドを用意しているわけではないのでありがたみがわかりづらいかもしれませんが(裏ではスキーマのメタデータからオブジェクトを生成したり、CRUD用のアクションを生成したりといった処理が行われています⁠⁠、インタフェースモデルを上手に利用すると、分業がやりやすくなるといったMVC的なメリットも享受しやすくなります(たとえば、インタフェースモデルのAPIをしっかり定義して、コントローラを書いている人にはDBIC固有のメソッドをいじらせないようにすると、モデルの担当者はコントローラやビューのコードをいっさい気にせずにスキーマのチューニングなどを行えるようになります⁠⁠。

ReactionとJifty

MooseベースのCatalyst 5.8が正式版となったいま、Mooseのメタクラスを使ったイントロスペクションを多用しているReactionは、先進的な事例としてまた少しずつ注目を集めるようになってきました。Catalyst関係の記事を集めているCatalystd.orgでもいくつか関連記事が取り上げられていますし、Mooseとサブルーチンアトリビュートの相性が悪いことから、Catalystコントローラのアクション定義もMooseやReactionのように宣言的な構文にしてはどうかという議論も出てきています[3]⁠。

ただ、この宣言的なフレームワークの分野では、Jiftyという、Reactionチームが誕生当初から強く意識してきた強力なライバルが存在しています。次回はその現状をおさらいしてみましょう。

おすすめ記事

記事・ニュース一覧