Liteでないアプリの実用的なディレクトリ構造一例

  • 30
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

Liteでないアプリの実用的なディレクトリ構造一例

普段は主にSMTPと付き合っててHTTPとはまだそれほど親しくなっていない@azumakuniyukiです。
昨日の@dokechinさんのエントリMojoliciousのJSON出力で文字化けの最後で

さて、次回はazumakuniyukiさんということで、期待高まります。

って書いてあって、ハードルが上がった気がしながら無駄に長くなった気がしないでもない12月6日金曜日の記事です。

Liteではないアプリケーション

WebアプリやWebサイトを作るのはたまにある程度なのですが、その時はMojoliciousを使っています。

Mojoliciousについて検索すると、比較的Mojolicious::Liteの記事が多いように思います。今日はLiteではない、ある程度の規模のアプリケーションを作ってみるという前提で実用的なディレクトリ構造について具体的なアプリを設計する感じで書いてみます。

市役所の配属先がネコ課

こういう話をする時は具体的な設定があった方が分かりやすい気がしないでもないので、市役所のネコ課に配属された担当者が市内の野良猫・地域猫の管理をするWebを作る事にします。

ネコ課の仕事は、市内の野良猫の数を把握し、野良猫を捕まえて去勢・避妊手術をして、地域猫化した上で再び捕まえた場所に帰したり、その過程で ネコの数を記録して管理 したりします。

ネコ課は人が少く、少人数で市内のネコを全て確認するのはかなり難しいので、市民からのネコ情報を受け付けられるシステムがあると仕事が捗ります。

ネコの管理アプリケーション、大雑把な要件

  • ネコ課の担当者が入力する画面(編集権限あり)
  • 市民がネコ情報を入力する画面(写真投稿と場所、時間等制限された項目のみ入力可)
  • 全てのネコ情報はネコ課の担当者のみが閲覧可能
  • 外国人居住者や観光客にも入力してもらうので英語のページも要る
  • ネコが自分で登録する事も出来る(寄ってきた野良猫にタブレット画面をタッチさせる)

ネコの管理アプリケーションの作成

セットアップ

Webアプリの本体はゆーすけべーさんの記事にも紹介されている通り、Noraneko::Webとします。

% mojo generate app Noraneko::Web
...
% cd ./noraneko_web

テンプレート

とりあえず京都市をイメージしてください。京都市では今回作るシステムの対象となるような地域猫を まちねこ と呼んでいます。

京都市は外国人居住者や外国人観光客が多いので、英語のページもあった方が良いです。それと、管理者用ページ(ネコ課の担当者が使う)とユーザ用ページ(市民がネコ情報を入れる)と動物病院からのネコ情報のテンプレート、それからネコが肉球で登録出来るページ(タブレットを設置して画面をタッチすると写真と位置情報が自動で登録されるやつ)も分離しましょう。

% mkdir -p templates/ja/{admin,user,clinic,cat}
% mkdir -p templates/en/{admin,user,clinic,cat}

lib/以下のモジュール(コントローラとコントローラ以外)

Webアプリケーションを作るのですが、Webとは関係のない機能も作るでしょう。

例えば夜間に走らせるバッチ処理なんかはスクリプトを動かしてDBのデータを更新するだけなのでWebは関係ありませんし、昨日入力されたネコ情報のレポートをメールで送る機能もWebは関係ありません。

lib/
  Noraneko.pm
  Noraneko/
    Web.pm ----------------- mojo generateで作られたアプリ本体
    Web/ ------------------- この下にWeb関係のコントローラを格納する

      User/ ---------------- 市民や観光客用
        Root.pm ------------ メニューとか
        StrayCat.pm -------- 市民が目撃したネコ情報を入力
        WantCats.pm -------- ネコを引き取りたい人が見るページ(ネコの写真が出る)

      Admin/ --------------- ネコ課担当者用
        Root.pm ------------ メニューとか
        Auth.pm ------------ ネコ課担当者のログイン関係処理
        StrayCat.pm -------- 野良猫情報を入力
        LocalCat.pm -------- 避妊・去勢手術済のネコ(地域猫)情報を入力
        HouseCat.pm -------- 飼い猫になったネコの情報を閲覧する
        WantCats.pm -------- ネコを引き取りたい人用のチラシを出すページ
        Statistics.pm ------ 統計情報を見る
        Maintenance.pm ----- メンテナンス中ページ

      Clinic/ -------------- 動物病院さん用
        Root.pm ------------ メニューとか
        Auth.pm ------------ 動物病院さんのログイン関係
        StrayCat.pm -------- 病院に連れてこられた野良猫情報を入力
        WantCats.pm -------- ネコを引き取りたい人用のチラシを出すページ

      Cat/ ----------------- ネコ用
        StrayCat.pm -------- ネコが自分の肉球で登録する用

    DB/ -------------------- 各テーブルの定義(何かORMを使う前提で)
      Administrators.pm ---- ネコ課の担当者一覧
      Clinics.pm ----------- 協力してくれる動物病院さん
      StrayCats.pm --------- 目撃されたネコとかネコ情報がどんどんたまる
      Cats.pm -------------- 個体を特定したネコを登録するとこ
      HouseCats.pm --------- 無事に飼い猫となったネコ情報
      Kinds.pm ------------- ネコの種類を定義するとこ(三毛猫・白猫・雉虎とか)

    Mail/
      Report.pm ------------ 前日のネコ情報をレポートで送信するとか

    Batch/
      CheckParent.pm ------- 目撃場所と色と模様から親子関係を推定するバッチ
      DeleteDuplicated.pm -- 重複する野良猫情報をマージしたり削除したり

そもそも管理ページとユーザ用ページは別アプリケーションにすべきという意見もあるかもしれませんが、ここでは Noraneko::Web の中に全て収容しています。

Webというディレクトリ

WebというディレクトリはこのAdvent Calendarの主催者であるゆーすけべーさんYAPC::Asia 2013で発表された通り、Noraneko/直下にStrayCat.pmやAuth.pmを置いてしまうと、Webに関係あるものないものがごっちゃになりますし、ファイル数が多くなれば全体の見通しも悪くなります。

12月4日の@xtetsujiさんのエントリmorboとhypnotoadの違いとハマりどころでも言及されていますので、このやり方がスタンダードと言っても過言ではないでしょう。

操作する人別ディレクトリ: User/, Admin/, Clinic/, Cat/

例えばStrayCat.pmは目撃された野良猫の情報を受けとりDBに記録します。もしWebというディレクトリ直下にStrayCat.pmがあれば、ネコ課担当者も市民も動物病院も全ての野良猫情報の入力をこのコントローラで処理する必要が出てきますので、将来的にコントローラ肥大化に繋がります。

野良猫情報を入力するのは市民・ネコ課担当者・唐物病院・ネコと様々ですが、目的は同じであっても入力出来る項目や編集可否等、入力者によって異なる点を綺麗に分離するなら、上記のようにUser/,Admin/のようにわけたほうが良いでしょう。

ログイン処理を行うAuth.pmもUser,Admin,Clinicにそれぞれありますが、やはり同じで、Web/Auth.pmとなっていると、sub userlogin {...}, sub adminlogin {...}って感じでわけて実装する事になりそうですが、ファイルが太りそうです。

Noraneko/Web.pmでURLを定義する

lib/Noraneko/Web.pm
package Noraneko::Web;
use Mojo::Base 'Mojolicious';

sub startup {
  my $self = shift;

  my $r = $self->routes;

  my $ctrl = undef;           # 長いコントローラ名入れる用
  my $lang = [ 'ja', 'en' ];  # 対応している言語を列挙

  $r->route('/:lang/user/index','lang' => $lang)->to('user-root#index');

  $ctrl = 'user-stray_cat';
  $r->route('/:lang/user/straycat/found','lang' => $lang)->to($ctrl.'#found');
  $r->route('/:lang/user/straycat/input','lang' => $lang)->to($ctrl.'#input');
  $r->route('/:lang/user/straycat/thank','lang' => $lang)->to($ctrl.'#thank');

  $ctrl = 'admin-stray_cat';
  $r->route('/:lang/admin/straycat/found','lang' => $lang)->to($ctrl.'#found');
  $r->route('/:lang/admin/straycat/input','lang' => $lang)->to($ctrl.'#input');
  $r->route('/:lang/admin/straycat/thank','lang' => $lang)->to($ctrl.'#thank');
}

Noraneko::Web::User::Root

最初のルート定義は、市民用メニューページのコントローラです。

lib/Noraneko/Web.pm抜粋
$r->route( '/:lang/user/index', 'lang' => $lang )->to( 'user-root#index' );

コントローラ名をuser-rootと記述すると、大元のNoraneko::Webの配下の名前空間としてUser::Rootが付けられ、Noraneko::Web::User::Rootがコントローラとして選択されます。アクション名のindexはNoraneko/Web/User/Root.pmのsub index {...}となります。

コントローラ本体では以下のようにテンプレートを指定してreturnしています。

lib/Noraneko/Web/User/Root.pm
sub index {
  my $self = shift;
  my $lang = $self->stash('lang'); # :langの中身
  # 何か処理、いろいろ。
  return $self->render( 'template' => $lang.'/user/root/index' );
}

Noraneko::Web::User::StrayCat

続いて市民が目撃した野良猫情報を写真と共に入力するページ用コントローラです。ここでは何回もコントローラ名を使うので$ctrlに入れています。

lib/Noraneko/Web.pm抜粋
$ctrl = 'user-stray_cat';
$r->route('/:lang/user/straycat/found','lang' => $lang)->to($ctrl.'#found');

上述のNoraneko::Web::User::Rootと違うのは、user-stray_catとしている点です。Mojoliciousのコントローラは先頭の一文字を大文字にしたものをコントローラ名として探すので、例えばuser-straycatと書いた場合は、Noraneko::Web::User::Straycatとなって (cが小文字) しまいます。

用意したファイルはNoraneko/Web/User/StrayCat.pmなので、CatのCを大文字にしたコントローラ名を探して欲しい場合は、user-stray_catのように、大文字にして欲しい先頭以外の文字の直前に _ (アンダースコア)を入れます。

'lang' => $lang

:langというプレースホルダをURLの中に定義していますが、後ろで:langが取れる値の一覧として$langを参照しています。例えば/de/user/indexにアクセスすると、ドイツ語には対応していないので、Not Foundとなります。

lib/Noraneko/Web.pm抜粋
  my $lang = [ 'ja', 'en' ];  # 対応している言語を列挙
  $r->route('/:lang/user/index','lang' => $lang)->to('user-root#index');

コントローラの中身で毎回jaかenかチェックしなくても良いので便利です。

Noraneko::Web::Admin

ネコ課担当者が使う管理ページも市民用と同じようなURL構造にしています。このように目的と対象と操作する人を分けたURLやコントローラ名で設計しておくと、全体の見通しが良くなります。

$r->namespaces()

ネコの管理アプリでは$r->namespaces()は使っていません。

これはコントローラを探す為の名前空間を登録する為のメソッドですが、次のようなコードは全てネコ課担当者用のコントローラに行ってしまいます。

$r->namespaces( [ 'Noraneko::Web::User' ] );
$r->route('/:lang/user/straycat/found','lang' => $lang)->to('stray_cat#found');

$r->namespaces( [ 'Noraneko::Web::Admin' ] );
$r->route('/:lang/admin/straycat/found','lang' => $lang)->to('stray_cat#found');

市民用コントローラNoraneko::Web::User::StrayCatに行く為の良い解決策が見つからなかったので、コントローラ名をuser-stray_catのように記述する事にしました。

github

一応、ちゃんと動くか心配なのでここで説明している箇所だけ実装したネコの管理アプリをgithubに置いてきました。

github.com/azumakuniyuki/Noraneko-Mojo-Sample

まとめ

最初は小さなアプリケーションを作るつもりであっても、意外にヒットして機能拡張工事をする事もあると思います。いざ、機能拡張する時にコントローラやWeb以外のファイルが増えても、メンテナンス性や全体の見通しが悪くならないようなURL設計、ディレクトリ設計を念頭に置くとよいと思います。

このように拡張する事を念頭に置いた設計であれば、次は殺処分されるネコを減らしたい保健所用コントローラとテンプレートを増やしたり、ネコの保護に熱心な市議会議員さん用のそれらを増やしたりする事になっても安心です。

僕はWebから遠い位置にいるので、そうそうWebアプリケーションを作る事はないのですが、何かを作る時は、例え管理ページを作る予定がなくても、例えば日本語ページだけであってもここで書いたようなディレクトリ構造で作っています。

本記事がそのような設計の参考になれば幸いですし、みんなはどんなURL構造・ディレクトリ構造で作っているのかというのも気になりますし、見てみたいと思います。

参考

避妊・去勢手術が終わった地域猫は区別しやすいように耳がカットされている。この猫の場合は左耳がカット済。耳カットの有無で手術済かどうかがわかるので、毎回ネコを捕まえてお腹を開いて確認するというネコにかかる負担が軽減出来る。

耳カットされた地域猫

この投稿は Mojolicious Advent Calendar 20136日目の記事です。