CakePHPを使ったMVC設計のベストプラクティス

個人的にはCakePHPはあまり好きではないのですが、CakePHP開発メンバーによるMVCデザインの記事 (CakePHP のおいしい食べ方)で紹介されていたBest Practices in MVC Design with CakePHP (php|architect’s C7Y)はMVCフレームワーク利用者にとってとても有用な情報だったので、訳してみました(php|architectの方には翻訳許可を頂いています)。

この記事を読んでドメインモデルに興味を持った方は、エンタープライズ アプリケーションアーキテクチャパターン(PoEAA)Domain-Driven Design: Tackling Complexity in the Heart of Softwareに手を出してみるのもいいかも。他に、InfoQにユーザー登録すれば、Domain Driven Design Quicklyという書籍のPDFが無料でダウンロードできるので、こちらもおすすめ。


Best Practices in MVC Design with CakePHP

by Nate Abele (2008-03-10)
Copyright © 2002-2008 Marco Tabini & Associates, Inc. — All Rights Reserved

このところのMVCフレームワークの流行もあって、みんなMVCパターンとその仕組みには馴染みがあると思う。たぶん、そういったフレームワークの上でアプリケーションを書いたこともあるだろう。しかしながら、MVCを採用した場合にもそのパターンが持つポテンシャルを発揮させることなく、「ページ指向の設計」のようなPHPにおける典型的なやり方をとってしまうPHPプログラマは多い。

今日はCakePHPを使った簡単なサンプルコードを取り上げて、MVCとその設計思想についてのよりよい理解を深めてみよう。MVCはデザインパターンであり、他のデザインパターンと同様に「ただひとつの真の実装」といったものは存在しないということを肝に銘じておくこと。MVCはもともと、「Web」がティム・バーナーズ=リーの眼前に現れるよりもはるか前の1979年にSmalltalkの開発過程において考案された。もともとの実装と現在Webフレームワークの中で見られるものには注目すべき違いもいくつかあるが、今回は取り上げないでおく。憶えておくべき重要なことは、パターンの実装に「たったひとつの正しいやり方」といったものはないということだ(無数の間違ったやり方が存在しているとも言える)。

CakePHPはほとんどのWebフレームワークと同様に、コントローラがリクエストに応答して必要であればモデルとのやり取りを行なうという構造になっている。コントローラはそのデータをビューに受け渡し、データが表示される。

PHP界での(ほぼ間違いなく)最大の問題があるとすれば、それはスパゲッティコードだ。それも、特にビューロジックとビジネスロジックが混ざり合ったものだ。我々は通常、コントローラとビュー層の分離をもっとも気にかける。それ自体は良いことなのだが、多くの場合それはモデルとコントローラとの分離をおろそかにすることになってしまう。その結果、肥大化したコントローラと貧弱なモデル、それにメンテ不可能なコードができあがる。以下のコードを見て欲しい:

/* controllers/posts_controller.php */
 
class PostsController extends AppController {
 
  public function index() {
    $posts = $this->Post->find("all", array(
      "limit" => 10, "order" => "Post.created DESC"
    ));
    $this->set(compact("posts"));
  }
 
  public function feed() {
    $posts = $this->Post->find("all", array(
      "limit" => 50, "order" => "Post.created DESC"
    ));
    $this->set(compact("posts"));
  }
}

このコードにはいくつかまずい点があるがそれには目をつぶってもらって、実際に行なっていることについて注目して欲しい。/posts/index(または/posts)というURLは、指定されたクエリパラメータ(“limit” => 10, “order” => “Post.created DESC”)というクエリパラメータを使ってレコードを取得し、それを$posts変数にセットすることで最新の記事10件を含むページを表示する。変数は、set()メソッドによってビューに送り出される。

メソッド名から推測できるとおり、/posts/feedというURLもRSSフィードを表示するということ以外はほとんど同じことをする(話を簡単にするためビューのコードは載せないので、想像で補ってほしい)。フィードは最新の50件の投稿を表示する。

さて、これはシンプルで明白な例ではあるが、このやり方はアプリケーション内にロジックの重複を生みだしてしまう。モデルは単なるデータ保存場所ではなくて、アプリケーションのドメインエンティティなのだ。テーブルとのやりとりにモデルの機能はとても重宝するが、モデルをそのためだけに使っているのであれば、あなたは損をしていることになる。ちゃんとしたMVCではモデルこそが第一級の存在であり、そのように扱われる。コントローラはデータをモデルから取得してビューへと送り出すだけのシンプルな糊として振るまい、アプリケーション内で最も魅力のない部分となる。

ロジックをどこに配置すべきか決断するときに使える便利な経験則: モデルに置けるあらゆるものは、そうすべきである(少なくとも「モデルかコントローラか」といった場合には。やり過ぎてビューロジックをモデルに置いた人を見たこともあるが…)。アプリケーションの核となるビジネスロジックと連携しないものは(セッション管理、リクエスト・レスポンス処理、セキュリティやアクセス制限に関するもの)、コントローラに置いたままにすべきである。それ以外のあらゆるものはモデルに放り込め。

さて、先ほどのコードをこの方針に沿って書き換えるとどうなるか?

/* models/post.php */
 
class Post extends AppModel {
  protected $order = "Post.created DESC";
}
 
/* controllers/posts_controller.php */
 
class PostsController extends AppController {
 
  public function index() {
    $posts = $this->Post->find("all", array(
      "limit" => 10
    ));
    $this->set(compact("posts"));
  }
 
  public function feed() {
    $posts = $this->Post->find("all", array(
      "limit" => 50
    ));
    $this->set(compact("posts"));
  }
}

出だしとしては悪くない。このように、CakePHPではデフォルトの$orderプロパティをモデルオブジェクトに持たせることができるので、クエリに含まれていた共通の部分を取り除くことができた。

しかし、まだ、ほとんど同じようなことをしている2つのメソッドが残っている。それは最初は目につかなかったかもしれないが、宿敵の帰還である: 私たちはビューロジックとビジネスロジックを混在させている。いずれのメソッドも、投稿の一覧を提供するという本質的には同じ処理を行なっている。表示部分とクエリが異なるが、基本的な処理は同じだ。

幸いなことにCakePHPはファイル拡張子を利用することで、同じアクションから複数のコンテンツタイプ(Content type)を返すことができる。ファイル拡張子は、設定ファイルroutes.phpに次の行を追加することで有効にできる:

/* config/routes.php */
 
Router::parseExtensions();

詳細は今後のチュートリアルで説明するとして、今はコントローラのindex()アクションを/posts.rss(または/posts/index.rss)でもアクセスできるようにするもの、とだけ憶えておけばよい。こんな感じになる:

/* controllers/posts_controller.php */
 
class PostsController extends AppController {
 
  public $components = array("RequestHandler");
 
  public function index() {
    switch ($this->params['url']['ext']) {
      case 'html':
        $options = array("limit" => 10);
      break;
      case 'rss':
        $options = array("limit" => 50);
      break;
    }
    $posts = $this->Post->find("all", $options);
    $this->set(compact("posts"));
  }
}

だいぶ良くなった。2つの似たようなアクションを1つにまとめたし、RequestHandlerコンポーネントを利用してテンプレートの切り替えをアプリケーションのコードから取り除くこともできた。RequestHandlerコンポーネントの働きについても後のチュートリアルで触れるが、本質的には同じアクションが複数のテンプレートを持てるようにして、コンテンツタイプに応じてそれらを切り替えるということをやっている。 さらに改良していけるかやってみよう:

/* controllers/posts_controller.php */
 
class PostsController extends AppController {
 
  public $components = array("RequestHandler");
 
  protected $_types = array(
    "html" => array("limit" => 10),
    "rss"  => array("limit" => 50)
  );
 
  function index() {
    $type = $this->params['url']['ext'];
    $posts = $this->Post->find("all", $this->_types[$type]);
    $this->set(compact("posts"));
  }
}

素晴らしい。コントローラのコードがより短く読みやすくなっただけでなく、表現力も上がっている。しかし、RSSフィードと同じデータを表示するブログのインデックスページがあるというのもちょっと冗長だ。代わりに、最新の人気記事をHTMLで表示したくなったらどうする?

switch文を使ったやり方に後戻りせずに問題を解決する方法はいくつかあるが、ここではカスタムfindメソッドについて掘り下げてみよう。CakePHP 1.2 betaでは新しいカスタムfindの書式が追加されており、findAll()のようなメソッドを呼んでいた箇所でfind(“all”)のように呼べる。これによって、より柔軟で再利用性の高いコードを書くことができ、ドメインエンティティにおけるビジネスロジックのカプセル化促進にも繋がる。それでは、我々のPostモデルに適用してみよう:

/* models/post.php */
 
class Post extends AppModel {
  protected $order = "Post.created DESC";
 
  public function find($type, $options = array()) {
    switch ($type) {
      case "popular":
        return parent::find('all', array_merge(
          array(
            'limit' => 10,
            'order' => 'Post.view_count DESC'
          ),
          $options
        );
      default:
        return parent::find($type, $options);
    }
  }
}

見て分かるように、デフォルトでいくつかのクエリパラメータを設定したカスタムのtype検索メソッドを作成した(デフォルトのパラメータは$optionsに設定したキーによって上書きできる)。ほんの少し創造力を働かせれば、多くの箇所でコードを減らすためにこの手法が適用できる。今回も、コントローラはシンプルな構造を維持したまま、ちょっと変更するだけで済む。

/* controllers/posts_controller.php */
 
class PostsController extends AppController {
 
  public $components = array("RequestHandler");
 
  protected $_types = array(
    "html" => array("popular"),
    "rss"  => array("all", "limit" => 50)
  );
 
  function index() {
    $options = $this->_types[$this->params['url']['ext']];
    $posts = $this->Post->find($options[0], $options);
    $this->set(compact("posts"));
  }
}

バッチリだ。我々のシンプルな糊(コントローラ)はいい感じで最小限、主要なビジネスロジックはドメインオブジェクト内にカプセル化。そして、実に説明的なメソッド呼び出しの構文も手に入った。

シンプルな法則(とクールなCakePHPの機能)をどのように取り入れれば、アプリケーションを単純にして、メンテナンスの必要なコードの量をとにかく減らすことができるのか理解してもらえただろうか。今後のチュートリアルでは、コードの再利用性を高めるためにMVCの各層を拡張する他の方法について取り上げるつもりだ。

Comments are closed.