FuelPHPのMVCについてちゃんと考えてみた
フレームワークというものを学びはじめてざっと5ヶ月くらいになるんですが、FuelPHPに限らず、CakePHPやRuby on RailsなどのWebアプリケーションフレームでは必ず登場するキーワード「MVC」についてちょっと理解が浅かったので、改めて色々調べてみました。
ようやく自分なりに納得のいく答えができたのでメモとしてまとめておきます。なお、具体的なプラクティスに関する知識は皆無な身の上ですので、「俺はこうやったほうが好き」といったご意見は大歓迎です。ぜひコメントでもTwitterでもいいので一言いただけばと思います。
MVCとは
MVCとはModel, View, Controllerの頭文字をとった言葉で、アプリケーション開発における役割の分離を実現するためのソフトウェアアーキテクチャです。簡単に言ってしまうと
といった感じです。もともとはSmalltalkから生まれたアーキテクチャで、後々にWebアプリケーションへと拡張されていきました。ここまではよくある解説で、大抵の人の認識と齟齬ないかと思いますが、これだけでは実装に至るには説明が不十分です。
MVCの勘違いとMVC2
さて、FuelPHPなどのWebアプリケーションフレームワークで実際にMVCを実現しようとすると、各コンポーネントはPHPのクラスで実装されますので、安直に実装すると以下のようなイメージになります。
HTTP経由でControllerのactionメソッドが実行され、受け取った値などをModelのメソッドに投げて返り値をControllerで受け取ります。データ加工を終えた段階でViewに加工済みのデータとテンプレートを指定して返す、という実装になるかと思います。
ただ、この実装には問題があり、MVCの実装におけるバッドプラクティス、ファットコントローラーを生み出しやすくなります。ファットコントローラーの何がいけないのかといえば説明するまでもないと思いますが、無駄にステップ数が増えたり、状態を持ちすぎたりすることでバグの温床になります。
そもそも、本来のMVCではViewはModelから状態を参照するべきであり、Modelは変更をViewに通知したり、Viewの変更をControllerに通知するようなオブザーバーパターンを実現できるべきなのです。オブザーバーパターンはWebでの実現が難しいため、これを排除したものをMVC2と呼びます。
MVCとMVC2どっちがいいのかという議論はとりあえず抜きにして、この時点で前述した実装とは異なることがわかるかと思います。ControllerはModelとViewを操作するべきですが、ViewはModelを参照するイメージでなくてはいけないのです。
ではModelはどうあるべきなのか?
とはいえ、Viewにはクラスがありませんし、ViewからModelのデータを呼び出すようなコーディングはFuelPHPではできません。ではどうあるべきなのでしょうか?
難しいところですが、個人的に納得したのは「Modelはある状態とそれを操作するメソッドを持ったオブジェクトであるように実装し、ControllerでViewを生成するときの引数にModelのアクセサの返り値を指定する」というスタンスです。
Modelの状態はクラスのプロパティでもよいですし、永続的なデータ保持を考えるならデータベースでも、何ならどこかのウェブ上に保存されているデータでもよいです。これに対して、メソッドはプロパティに対してアクセサであり、データベースに対してはドライバであり、ウェブ上のデータに対してはAPIリクエストなどが考えられます。Modelはデータであり、インターフェイスでもあるわけです。ウェブ上のデータを「Modelとして保持」するのは少し違和感があるかもしれませんが、データの保存場所は問いませんので問題ありません。
そのため、常にModelとしてクラスを設計する際には「どんなデータをどんな形で保持する目的を持っているクラスなのか?」「Modelとしてデータに対する操作にはどんなメソッドが必要なのか?」を考えることが重要かと思います。
ちょっと以下のコードを見てみます。
<?php use \Model\Report as Report class Controller_Setting extends Controller { public function action_index($count, $filter) { $mode = Report::getReportMode($count, $filter); $report = Report::getReportData($mode); return View::forge('home', $report); } // 省略 }
上記は表示件数($count)とフィルタ設定($filter)から表示設定をセットし、レポートを生成して表示するようなControllerの実装サンプルです。とりあえず動きますが、前述したファットコントローラーになりやすいデータの受け渡し構造になってしまっています。これを「レポート表示に関する設定情報とレポートデータを保持するオブジェクト」としてModelを定義すれば、以下のように書き直すことができます。
<?php use \Model\Report as Report class Controller_Setting extends Controller { public function action_index($count, $filter) { Report::setReportMode($count, $filter); Report::createReportData( Report::get('mode') ); return View::forge('home', Report::get('report') ); } // 省略 }
いかがでしょうか。ステップ数こそ変わりませんがモデルをステートフルなオブジェクトだと考えることで前例ではわかりにくかった「レポートの表示モードの設定」と「レポートの作成」を明確にメソッド名として記すことができています。また、ControllerがModelのデータを操作する、ViewがModelのデータを参照する、という構造が見えやすくなり、Controller上での無駄な変数が排除できています。
実際には、毎回毎回アクセサを呼び出していると、それはそれで冗長な振る舞いになりますので、同じアクセサを複数実行するようなケースがあるならば、一度Controllerで受け取っておくのもいいと思います。
Modelの意味から考えるオブジェクト指向とアクセサ
先ほどはControllerでViewとModelのつながりを表現するためにアクセサを使っていましたが、別に直接プロパティを参照すればいいのでは?と思うかもしれません。確かに、メソッドの引数に別メソッドの返り値を直接入れ込むような書き方は副作用を連想させるあまり褒められた書き方とはいえませんから、プロパティを直接指定したほうがスマートに見えます。さらに言えば、表示モードの情報もModelが保持しているのですから、Controllerで受け取る必要もないでしょう。
<?php use \Model\Report as Report class Controller_Setting extends Controller { public function action_index($count, $filter) { Report::setReportMode($count, $filter); Report::createReportData(); return View::forge('home', Report::$report ); } // 省略 }
つまり上記のように変更するわけです、しかし、このコードにはいくつか難があります。
まず第一に、createReportDataメソッドがController内での別のメソッドの実行による状態変化に依存していることです。Modelはステートフルなコンポーネントですから、状態に依存することは当然といえば当然なのですが、Controller内の別メソッドの実行に依存するのはメソッドのテスタビリティを低下させたり、可読性を低下させます。
前述のコードでもModelにsetReportModeメソッドの実行結果を保持させていましたが、createReportDataメソッドの内部で参照するのでなく、Controllerで明示的にモードを取得してcreateReportDataメソッドに引き渡していました。こうすることで、メソッドのテスタビリティが向上し、引数以外の内部状態からcreateReportDataメソッドの実行結果が変化することがなくなります。つまり、メソッドがステートレスになるわけです。
ではもうひとつ、アクセサを使わないプロパティの参照はどうでしょうか。このコードでは特に問題はありませんが、要件が変更された場合に面倒です。例えば、レポートを一度表示させたら内部的にデータを削除するような仕様に変更された場合を考えてみましょう。
<?php use \Model\Report as Report class Controller_Setting extends Controller { public function action_index($count, $filter) { Report::setReportMode($count, $filter); Report::createReportData( Report::$mode ); $report = Report::$report; Report::deleteReportData($report); return View::forge('home', $report); } // 省略 }
レポートデータをControllerで受け取った後、レポートデータを削除するためのdeleteReportDataメソッドを実行し、ようやくViewに渡しています。
この記述の場合、Controllerが肥大化するだけに限らず、後々このコードをメンテナンスするエンジニアがうっかりdeleteReportDataメソッドを実行し損ねて思わぬバグを生み出すなんてことが考えられます。
こうならないためにアクセサを利用します。アクセサはControllerから見て、モデルの整合性を担保する要素になります。Modelに正しくアクセサを実装してあれば、Controllerからはどのような呼び出し方をしてもModelの整合性は損なわれないと断言できるようになるわけです。これがオブジェクト指向におけるカプセル化の考え方ですね。
この場合であれば、getメソッドの内部でdeleteReportDataメソッドを実行するようにしておけば、Controllerはレポートデータの取得だけに集中することができます。通常のデータ取得と、内部的にデータを削除するデータ取得とを明確に分けたいのであればメソッド名を工夫すればOKです。
staticであるべきなのか?
これまでサンプルコードでは、すべてModelの呼び出しを静的に呼び出していますが、別にnewでオブジェクトを生成してもいいかと思います。とはいえ、Controllerから複数のオブジェクトを意識しないといけないようなケースでなれば、静的に実装してもいいのではないかと個人的には考えています。
まとめ
もう少し経験積んで考え直したい
今までMVCについてはいまいちわからない感じでしたが、ようやく納得行くレベルまで理解できた気がします。とはいえ、まだまだ経験が足りないこともあって実践的なノウハウがつかめていません。
しばらくは自分なりにコーディングしてみたり、人のコードを読んだりして勉強するべきですね。GitHubとか見れば人の書いたコードが見れますから、Fuelのコアを読むくらいやったほうがいいなと感じました。
参考
基本: MVC のベストプラクティス | The Definitive Guide to Yii | Yii PHP Framework
# YiiですがMVCのベストプラクティスについて解説されており、極めて参考になります。
CakePHPを使ったMVC設計のベストプラクティス - Sooey
# こちらはCakeのベストプラクティス。徐々に改善していく流れがわかりやすいです。
PHPerのMVCの一体どこが間違っていたのか - MugeSoの日記
# MVCの勘違いをズバッと指摘してくれる良ポストです。一度は読むべきだと思います。
cakephpやRailsのMVCデザインパターンに関して - 田舎の技術者が奮闘中
# 同じくMVCの勘違いについてコメントされています。こういった気付きが重要なんだなと思います。
Life is beautiful: Ruby on Railsの「えせMVC」の弊害
# 少々過激なタイトルですが、Railsで発生しやすいMVCの勘違いを厳しく指摘しています。
「MVCの勘違い」について、もう一度考えてみる - 圧倒亭グランパのブログ
# MVCの勘違いについて図有りでしっかり解説されていてわかりやすいです。
MVC と MVC2 について改めて考えてみる - スタジオ・アルカナ技術ブログ
# MVCとMVC2について解説されています。MVC2については記事が少ないのでおすすめです。
オブジェクト指向再入門/なぜわからなくなってしまうのか?
# オブジェクト指向の勘違いについて触れていますので、わかったつもりな人でも一読をおすすめします。