モデルの細分化

MVCの弱点

筆者がMVCを利用するうち感じた事があります。
とりあえずVは置いといて、MとCだけでは足りないと感じる事があるということです。

モデルの設計の仕方は様々ですが、うまく設計しないとすぐにMVCの弱点が表れます。
フレームワークでは、RailsのActiveRecordのようにO/Rマッパとしての機能を持つクラスをモデルとして利用するケースが多いと思います。これはこれでいいのですが、テーブルに直接関わらないロジックを書く場所がなくなるという事をまず感じます。
そして迷ったあげく、コントローラーにそれを書いちゃったら最悪です。既にMとCの役割の境界が崩れてしまっています。これはRailsではよく見られる形ですが、実際にはRailsには罪はなく、多くの実装者の誤解によるものです。つまりモデル=データという誤解です。
モデル=データではありません。モデルはデータを含んだ世界ですが、決してイコールではありません。

そしてもう一つ、複数テーブルにまたがるトランザクション処理はどうするのかという事です。
モデル=データという誤解に基づくと必ずこの疑問にぶつかります。これも望ましくない結果としてトランザクション管理はコントローラー、という事になるパターンが多いように思います。

そこで浮かんでくるのがMを二段階にするということです。

モデルはデータベーステーブルとクラスを1:1で設計するといい感じになったりしますが、同時にメインロジックを担当すると言う大きな役目を持ちます。これの行き場をコントローラーにしてしまうのではなく、明確にモデルをメインロジックとデータという2段階にするのです。

論理モデルと物理モデルの2階層

「モデル」の章で物理モデルと論理モデルについて書いていますが、これらを両方取り入れます。
ただし双方を同列に位置づけたのではそれぞれの存在意義が薄れます。
そのため、論理モデルの下に物理モデルがぶら下がる様な設計とします。
つまり、アプリケーションの事象をそのままクラス化した「カート」のような論理モデルを基本として、カートに関わる処理はこれに集約します。
そして最終的なデータストアはやはりテーブルなので、リレーショナルデータベースの性質に基づいて、カートであれば「カート基本データ」と「カート内商品データ」という2つの物理モデルクラスも作成します。そして、「カート」クラスの処理の一部分として「カート基本データ」と「カート内商品データ」のデータを入出力するという事を行うようにします。

そしてコントローラーからは基本的には論理モデルである「カート」クラスのみ意識するようにします。
処理呼び出しの階層的には以下の様な感じです。

コントローラークラス
  ∟カートクラス(論理モデル)
    ∟カート基本データクラス(物理モデル)
    ∟カート内商品データクラス(物理モデル)

カートに関わる処理ロジックのフローを把握するのが論理モデルで、ここを見れば処理の一連の流れがつかめるようなイメージです。配下にある復数の物理モデルのトランザクションも管理します。

対して物理モデルは論理モデルより受け取ったパラメータをもとに、本当にSQL分の構築と実行のみを行います。論理モデルで何をやられているかは知ったことではない、くらいで良いと思います。

Cart.php
<?php

class Cart
{
    // カート情報を取得
    public function getCartInfo($userId)
    {
        $cart = new CartHeader();
        $cartDetail = new CartDetail();
        
        $cartInfo = $cart->getUserCart($userId);
        $products = $cartDetail->getProductList($cartInfo['cart_id']);
        $cartInfo['products'] = $products;

        return $cartInfo;
    }

    // 商品追加
    public function addProduct($cartId, $productId)
    {
        $db = new PDO();
        $db->beginTransaction()
        try
        {
            $cart = new CartHeader();
            $cartDetail = new CartDetail();

            // カートに商品を追加
            $cartDetail->add($productId);

            // カート情報の合計金額を更新
            $cart ->updateTotalPrice($cartId);

            $db->commit();
            return true;

        } catch (Exception $e) {
            $db->rollback();
            throw $e;
        }  
    }

}

?>
CartController.php
<?php

class CartController extends ControllerBase
{
    // カート商品一覧
    public function displayAction()
    {        
        $params = $this->request->getParam();
        // カート情報取得
        $cart = new Cart();
        $cartInfo = $cart->getCartInfo($params['user_id');
        $this->view->assign('cart_info', $cartInfo);
    }

    // 商品追加
    public function addAction()
    {
        $post = $this->request->getPost();
        // 商品追加処理
        $cart = new Cart();
        $cartInfo = $cart->addProduct($post['cart_id'], $post['product_id']);
        // カート商品一覧へリダイレクト
        header('Location: http://www.xxx.com/cart/display');
    }

}

?>

コントローラーはひたすら処理の呼び出しに徹します。
考え方としてリクエスト情報やセッションを直接操作するのはコントローラー、それらの情報を元に処理を行うのがサービスまたはモデルと考えます。
ちなみに物理モデルもれっきとしたモデルですので、テーブル単体で処理が完結するような場合は論理モデルを介さず、直接コントローラーから物理モデルを操作するのも構わないと思います。

ロジックを小分けにするということ

今回のように、MVCという3層の概念に加えて、更にMを2階層にするということは、やたらにクラスとファイルが増えて管理が面倒に思われるかもしれません。

それどころか、世の中のフレームワーク利用者には、コントローラーに直接ロジックをゴリゴリと書くというパターンも少なくないようです。多分、クラスが分かれれば分かれるほど、いろんなところに目をやらなくちゃいけなくなり、面倒になるというイメージがあるためかと思います。特にPHP覚え始めの方ほど、一箇所に詰め込むほうがコードが見渡しやすいと思いがちです。ですがこれは逆です。

慣れるほどに気づきますが、コードは小分けにしてあるほうが読みやすく、保守性はあがります。
探しているロジックがどのへんに記述されているか、ファイル名から判断がしやすく、検討が付けやすいためです。
ダラダラと長く書かれていると、それこそ全文検索みたいなもんです。
人間は全文検索よりも、インデックス検索のほうが得意なのです。
しかも役割で分けられているとなおさらです。

小分けにしてある方が、プログラムの修正に対する影響範囲も小さくなります。ただしこれは各クラスの役割分担が明確にされていればの話ですが。