手続き型とオブジェクト指向の違いをWebアプリで理解する
先日、FizzBuzzをオブジェクト指向でやってみたわけですが、今度は簡単なWebアプリケーションをオブジェクト指向で作ってみようと思い、やってみました。
サンプルとして作るアプリは、下のようなテーブルに対して、画面から参照/更新するという単純なものです。
Languageテーブル
No | Language |
---|---|
1 | Perl |
2 | Python |
3 | PHP |
4 | Ruby |
まずは以前と同じように、これを手続き型で作ってみましょう。
index.php(手続き型)
<?php //DBアクセス try { $dbh = new PDO('sqlite:language.db','', ''); if ( !empty($_REQUEST["value"]) ) { $sql = 'update languages set language = "'.$_REQUEST["value"].'" where no='.$_REQUEST["key"]; $stmt = $dbh->prepare($sql); $stmt->execute(); } if ( !empty($_REQUEST["key"]) ) { $sql = 'select * from languages where no='.$_REQUEST["key"]; } else { $sql = 'select * from languages'; } $stmt = $dbh->prepare($sql); $stmt->execute(); } catch (PDOException $exception) { var_dump($exception->getMessage()); exit(); } //画面表示 foreach ( $stmt as $rs ) { echo $rs[no].'=>'.$rs[language].'<br>'; } echo <<< EOF <form action="dbtest.php" method="get"> key:<input type="text" name="key" > <input type="submit" value="list"> <input type="hidden" name="action" value="list"> </form> <form action="dbtest.php" method="get"> key:<input type="text" name="key" > value:<input type="text" name="value" > <input type="submit" value="update"> <input type="hidden" name="action" value="update"> </form> EOF; ?>
クライアントから渡ってきたパラメータを元にして、SQLを組み立ててDBアクセスし、その結果を表示しています。
ソースコードが1つのファイルにまとまっていて、上から下へ処理が流れていくだけです。とても単純ですね。
ただ、今の段階ではアプリの機能が少ないので、このままでも充分見やすいソースコードになっているのですが、今後機能追加していくにあたり、内部の条件分岐が多くなってきて、だんだんとわけがわからなくなってきそうです。
そこでこれを、オブジェクトを利用したクラスベースの実装に変えてみましょう。
上の手続き型のソースコードを、「DBアクセスする部分」と「画面表示する部分」と「それらを制御する部分」の3つに機能分割し、それぞれを個別にオブジェクトとして扱うことで、うまくいきそうです。
クライアントから渡ってくるパラメータは「Model」「View」「Action」とし、それぞれのパラメータに応じたオブジェクトが連携を取りながら処理が進んでいくようにします。
index.phpはこんな感じです。
<?php function __autoload($className) { include_once($className.'.php'); } $paramobject = new Param(); $actionname = $paramobject->_param['action']; new $actionname($paramobject); ?>
まずは初期処理として、Paramオブジェクトを作成しています。
このParamオブジェクト内部では、クライアントから渡ってきたパラメータをチェックし、適切な値に変換して保持しています。
param.php
<?php class Param { var $_param; function __construct() { //ここで指定した正規表現にマッチしなければ、デフォルト値に置換する $regexpparam['action'] = '/^[0-9a-zA-Z_]+$/'; $regexpparam['model'] = '/^[0-9a-zA-Z_]+$/'; $regexpparam['view'] = '/^[0-9a-zA-Z_]+$/'; $regexpparam['key'] = '/^[0-9]+$/'; $regexpparam['value'] = '/^[0-9a-zA-Z_]+$/'; //デフォルト値 $defaultparam['action'] = 'list'; $defaultparam['model'] = 'language'; $defaultparam['view'] = 'list'; $defaultparam['key'] = ''; $defaultparam['value'] = ''; foreach ( $regexpparam as $key => $value ) { if (preg_match($value,$_REQUEST[$key])) { $this->_param[$key] = $_REQUEST[$key]; } else { $this->_param[$key] = $defaultparam[$key]; } if ($key == 'action' || $key == 'model' || $key == 'view') { if (file_exists($this->_param[$key].$key.'.php')) { $this->_param[$key] .= $key; } else { $this->_param[$key] = $defaultparam[$key].$key; } } } } } ?>
次に、Actionオブジェクトを作成し、そこに先ほどのParamオブジェクトを渡しています。
Actionオブジェクトの内部では、Modelオブジェクトが起動してDBアクセスを行い、その結果を保持します。
さらにViewオブジェクトが起動し、Modelオブジェクトから結果を受け取って画面表示を行います。
ここでの処理の流れは、クライアントからActionとしてList(またはUpdate)が指定されたので、Listaction(またはUpdateaction)オブジェクトが起動し、その内部でLanguagemodelオブジェクトがDBアクセスを行い、結果をListviewオブジェクトが画面表示しています。
listaction.php
<?php class Listaction { function __construct(&$paramobject) { $modelname = $paramobject->_param['model']; $viewname = $paramobject->_param['view']; $key = $paramobject->_param['key']; $modelobject = new $modelname(); $modelobject->read($key); $viewobject = new $viewname($modelobject); $viewobject->write(); } } ?>
updateaction.php
<?php class Updateaction { function __construct(&$paramobject) { $modelname = $paramobject->_param['model']; $viewname = $paramobject->_param['view']; $key = $paramobject->_param['key']; $value = $paramobject->_param['value']; $modelobject = new $modelname(); $modelobject->update($key,$value); $modelobject->read($key); $viewobject = new $viewname($modelobject); $viewobject->write(); } } ?>
languagemodel.php
<?php class Languagemodel { var $_dbh; var $_records; function __construct() { try { $this->_dbh = new PDO('sqlite:language.db','',''); } catch (PDOException $exception) { var_dump($exception->getMessage()); exit(); } } function read($key) { $sql = 'select * from languages'; if ( !empty($key) ) { $sql .= ' where no='.$key; } $this->_records = $this->_dbh->prepare($sql); $this->_records->execute(); } function update($key,$value) { if ( !empty($value) ) { $sql = 'update languages set language = "'.$value.'" where no='.$key; $this->_records = $this->_dbh->prepare($sql); $this->_records->execute(); } } } ?>
listview.php
<?php class Listview { var $_html; function __construct(&$modelobject) { foreach ( $modelobject->_records as $rs ) { $this->_html .= $rs[no].'=>'.$rs[language].'<br>'; } $this->_html .= <<< EOF <form action="index.php" method="get"> key:<input type="text" name="key" > <input type="submit" value="list"> <input type="hidden" name="action" value="list"> </form> <form action="index.php" method="get"> key:<input type="text" name="key" > value:<input type="text" name="value" > <input type="submit" value="update"> <input type="hidden" name="action" value="update"> </form> EOF; } function write() { echo $this->_html; } } ?>
手続き型で作っていた頃は、作り込んでいくうちにソースコードがどんどん汚れていったり、肥大化していくのがわかったのですが、オブジェクトを基準にして考えていくことで非常にコンパクトですっきりと見やすくなりましたね。
また、今回作ったアプリケーションですが、具体的な処理を行っている部分をきれいに取り除いてしまい、雛形の部分だけを抽出することで、次回に全く別のアプリを作成する際にも利用できそうな気がします。
そうやって取り出した雛形を、一般的に「フレームワーク」と呼ぶのだと思っているのですが、クラスがオブジェクトの雛形だとしたら、フレームワークはアプリケーションの雛形であると考えることができるでしょう。
RailsやCakeは、このような「雛形」としてのフレームワークに対して、ソース生成やテスト支援やORマッパーなどの機能を追加していったものなのだと思っています。
あと、もうひとつ私なりに気づいたことがあって、「フレームワーク」とは、目に見えている「雛形」のみを指しているわけではなく、「こういう場合はこういうことにしましょう」というルールを決めて、それを守っていくことも含まれているのだということです。
今回のサンプルでいうと、「クライアントから受け取るModel/View/Actionパラメータに対応するクラスを、同名のファイルに記述していく」というルールですね。
こういったルールに従うことで、ソースコードの可読性/保守性が高まり、ある程度まとまったチームで開発する際に威力を発揮するようになってくるのではないかと感じました。
せっかくなので、今回つくったアプリから雛形部分だけを取り出して公開しておこうと考えています。
その後、そのフレームワークを利用して、ちょっと面白いものを作ってみようと考えています。
乞うご期待です。