2007年11月25日

五十音パッドの検索部分の実装(多態編)

前回はswitch文で分岐させることによって異なる検索モード(WHERE句が微妙に違う)を実装してみましたが、案の定?というかやはりコードはみるみるうちに汚くなってしまいました。

初めてswitch文なるものを見たときはif文の羅列よりは随分整然とコードが書けるものだなぁ?などと思った記憶がありますが、改めて自分の書いたコードを眺めてみると、switch文が如何にコードを見難くしているかがわかります。

しかも下手に手続き型ではなくなっているため、却ってコードの見通しが悪くなっているというのか…。せっかくフレームワーク使ってMVCの分離だ!とかやっていても、これでは手続き型でダラダラ書いていた方がまだマシだったのではないか?と思わせられるぐらいです。

そこでフレームワークの設計もオブジェクト指向に基づいているのだろうし、ここはオブジェクト指向的にswitch文をポリモーフィズムで書換えてみようとおもいました。

と、思ったのはいいのですが、実はオブジェクト指向的なプログラムなんてほとんど経験がなく、PHPでクラスなんて作ったことないし、継承といっても各種フレームワークのマニュアルにそう書いてあるから仕方がないのでextendsとか書いてみただけだし…、ぐらいの経験しかありません。なので以下に書いてあることは間違っても鵜呑みにしないでください。

ということで自分のプログラムスタイル的にはここらへんまで考えたらとりあえず書いてみる!ってことでController(のファイル)の上の方(class Gappei extends Controllerの外側という意味)にクラスを書いてみました。
abstract class SearchMode
{
private $criteria;

function __construct(array $condition)
{
$this->criteria = $condition;
}

function getCriteria()
{
return $this->criteria;
}

function where()
{

}
}

class NormalSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{

}
}

class CapSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{

}
}

何故Controller(のファイル)に書いたかというと、とりあえずどこでインスタンス化するかなぁ?と考えたときに、たぶんControllerのどっか?だろうとおもい安易にここに書き連ねただけです。

構造?(クラス階層?)的にはSearchModeという抽象クラスを継承したNormalSearchとCapSearchというクラスがあるという形です。where()というメソッド内でそれぞれのWHERE句を生成する予定で一応ガワだけ書いておきました。

$criteriaは検索語句を格納する予定のメンバでNormalSearchの場合はpostで受け取った配列、capSearchの場合はURLの第3セグメントで受取った文字になります。ということでそれぞれをインスタンス化するコードを書きます。

function search()
$this->cond = new NormalSearch($this->input->post('condition'));

function kanaSearch($cap)
$this->cond = new CapSearch(array($cap));

$condはGappeiクラス内のメンバで型があるとすればsearchModeということになるっぽいですがPHPなのでよくわかりません。とりあえずこの変数にどちらかのインスタンスが格納されるってことで…。
private $cond;

で、この後はindex()で$condをごにょごにょするなりModelに送る?なり…と思ったのですが、↑の二つのメソッド内で
redirect('gappei/index/');

のようにindex()にリダイレクトしています。すると$condが初期化されちゃうんですね(--;。まあ当たり前といえば当たり前なのかな???というわけで、ってゆーかいずれにしろページングとかソートとかやったら↑の二つのメソッドを経由しないで(つまりインスタンスが作られないまま)index()を呼ぶことになってしまうわけで、これはどっちにしろオブジェクトをどこかに保存しておく必要があるというわけです。なので…

//検索オブジェクトセット
$this->phpsession->save('searchObj', serialize($this->cond));

のようにして今までと同じようにシリアライズしてオブジェクトを丸ごとセッションデータに格納することにしました。

次にModelのクエリ組立部分で前回はswitch文を使用していたところをばっさりと削除して、次のように変更します。
$searchObj = unserialize($this->phpsession->get('searchObj'));
$searchObj->where();

シリアル化を元に戻してオブジェクトを取得し、それのwhereメソッドを呼ぶだけです。あとはwhere()の実装です。

まずNormalSearchの方ですが元々は
$this->db->like(array_diff($criteria, array('', NULL)));

と書いていました。$criteriaはメソッドの引数として受け渡されたものだったので、これは今度はクラスのメンバ、つまりgetCriteria()を呼べばよいということになります。

が、問題なのは$thisでこれはModel内で書いてあったから許される?っつーか、たぶん?いやほぼ確実にこのままじゃダメっぽいということはわかります。

んでユーザガイドを見てみたらライブラリの作成のところに
$CI =& get_instance();

のようにすれば、$thisのかわりにCodeIgniterのネイティブなリソースにアクセスできる旨が書いてあったのでそうします。

ということで結局できあがったNormalSearchクラス
class NormalSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{
$CI =& get_instance();

$CI->db->like(array_diff($this->getCriteria(), array('', NULL)));
}
}

データベースのライブラリはオートロードする設定になっているのでここには書かなくても大丈夫みたいでした。でcapSearchクラスも同じような感じで…
class CapSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{
$CI =& get_instance();

$field = get_cookie('kanatab') == 'ocap' ? 'oyomi' : 'yomi';

$CI->db->where(array("SUBSTRING($field FROM 1 FOR 1) = " => $this->get1stCriteria()));
}
}

ここでget1stCriteria()というのは親クラスに新たに作成したメソッドで、$criteriaは配列ということにしてあるので、それの最初の要素を返すだけのメソッドです。
function get1stCriteria()
{
$temp = $this->criteria;
return $temp[0];
}

db->where()に渡している配列は、キーに演算子を含ませています。試したところどうやらキーにスペースが含まれている場合は演算子'='は出力されなかったのでこのようになっています。スペースが含まれていない、例えばarray('pref'=>'あ')のような配列だと、自動的に'='を含む pref = 'あ'のようなWHERE句を生成してくれるみたいです。

$fieldというのは五十音パッドのタブがどちら([新/旧]市町村)を選択されているかで、取ってくるフィールドの名前(yomi/oyomi)が異なるためこのようになっています。これはデータベース設計を見直せばいらない部分かもしれません。が、とりあえずこのままでいきます。

最後にindex()の件数取得部分でModelのgetCount()に渡していた引数$criteriaを削除し
$data['count'] = $this->Gdb->getCount($data['sort_key']);

Modelの方も関数の頭の部分を変更します。
function getCount($sortkey)

これでデータ取得は問題ないのですが、ビューに検索語句を渡さなければならないため、結局セッションデータから検索オブジェクトを取得してそれをビューに渡す変数$dataに格納するという処理を書かなければなりませんでした。
//検索オブジェクト取得
if ($this->phpsession->get('searchObj') <> '')
{
$searchObj = unserialize($this->phpsession->get('searchObj'));
} else {
$data['criteria'] = array('pref' => '', 'nc' => '', 'oc' => '');
$this->load->view('no_session', $data);
return;
}

//検索語句取得〜Viewに設定
if (get_class($searchObj) == 'NormalSearch')
{
foreach ($searchObj->getCriteria() as $key => $val)
{
$data['criteria'][$key] = $val;
}
} else {
$data['criteria'] = array('pref' => '', 'nc' => '', 'oc' => '');
}

最初のif文でセッションデータの有無を調べてあれば$searchObjに格納しています。初回アクセス時などセッションデータがない場合の処理で、その際はno_sessionという検索フォームと五十音パッドだけのビューに転送しています。

次のif文で検索語句を取得し$data配列に格納しています。ここでの条件判断で使用しているget_class()はインスタンスのクラス名を返すPHPの組込関数です。

いずれも検索語句がない場合に配列を初期化していますが、この処理の書き方はなんか汚いというか長くなる原因というか…後々の課題ってことで…。

それにしてもこれらの処理(ビューに渡す)がなければかなりすっきりしたのですが…。それはともかく最終的には独自クラスを別ファイルにしてController(のファイル)からrequireする形にしました。
require_once('search.php');

で、一応これで動いてます。switch文も完全になくなりました。でも自分で書いておいてよくわからない(ってゆーか不安な)部分があるのでメモっておきます。

Model内でクエリ組立てる部分がその他のところは$this->db〜のように書いているのに、オブジェクトを使った部分では$CI->db〜になっているわけで、これはきちんと同じdbオブジェクト?を指しているのだろうか?ということ。

これはなんらかのデザインパターンに当てはまる形なのか?ということ。なんとなくステートパターンとかいうのに似てる気もしないでもないが???

あとどう考えても再利用とかできそうにないんですが、こんなもんなのか?ということ。

いずれにしても自分にとって初めて動いたオブジェクト指向的プログラムなので、くれぐれも真似しないように(TT。(動きゃぁいいんだぃ!)
ラベル:codeigniter PHP
posted by ciallost at 00:05| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック