CakePHPのヘルパーをStrategyパターンで実装する方法[1.3][Helper][デザインパターン]
Webアプリケーションを開発する際、認証機能も実装する事は今やほぼ必須です。
CakePHPでは簡単に認証機能を実装できるAuthコンポーネントをデフォルトでサポートしています。
このため、CakePHPにおいて、開発者はほとんど負担なく認証機能を実装することができます。
しかし、問題はViewレイヤーです。
ログイン状態の出力をViewファイル中に埋め込んだPHPでIF文でいちいち制御するのは最高に面倒です。
LoginHelperとLogoutHelperと2ファイルに分けて使い分けるのも手ではありますが、美しい解決ではありません。
私はこの問題の解法としてデザインパターンの1つ、「Strategyパターン」を用いる方法を紹介したいと思います。
ちょっと歪な設計になりましたが。。。
引用
Strategyパターンとは?
Strategyパターンの目的は、GoF本では次のように定義されています。
アルゴリズムの集合を定義し、各アルゴリズムをカプセル化して、それらを交換可能にする。
Strategyパターンを利用することで、アルゴリズムを、それを利用するクライアントからは
独立に変更することができるようになる。Strategy パターンは、オブジェクトの振る舞いに注目したパターンです。
Strategyパターンでは、それぞれの処理をクラスとして定義します。その際、クライアントにアクセスさせるための共通APIを用意しておくのがポイントです。これにより、処理クラスを利用する側は具体的な実装を意識することなく、共通のAPIで処理を実行できます。
また、処理の実行を処理クラスのオブジェクトに委譲することで、処理の切り替えができるようにしています。
Strategyパターンの構造
Strategyパターンのクラス図と構成要素は、次のとおりです。
Strategyクラス
それぞれの処理に共通のAPIを定義します。Contextクラスからは、Strategyクラスで定義されたAPIを通じて、ConcreteStrategyクラスで提供される具体的な処理を呼び出します。
ConcreteStrategyクラス
Strategyクラスのサブクラスで、Strategyクラスで定義されたAPIを実装したクラスです。このクラスに具体的な処理内容を記述します。
Contextクラス
Strategy型のオブジェクトを内部に保持し、具体的な処理をそのオブジェクトに委譲します。こうすることで、ConcreteStrategyクラスに依存することがなくなりますので、ConcreteStrategyクラスを切り替えることができます
Strategyパターンのメリット
Strategyパターンのメリットとしては、以下のものが挙げられます。
処理毎にまとめることができる
それぞれの処理がクラスにまとめられて実装されており、コードは処理内容に専念することができます。これにより、保守性が高まります。
また、新しい処理が追加された場合も、既存のコードに手を入れることなく、新しいクラスを作成するだけで済みます。
異なる処理を選択するための条件文がなくなる
1つのクラスやメソッドに異なる処理を記述した場合、if文やswitch文を使って処理を分岐することになります。これは、コードの可読性を落とすため、保守性・拡張性が下がります。Strategyパターンを適用すると、処理がクラス単位にまとめて実装されます。この結果、if文やswitch文を使うことがなくなり、非常にすっきりしたコードになります。
異なる処理を動的に切り替えることができる
クラス単位に処理がまとめて実装されているので、クライアントは使いたいConcreteStrategyクラスのインスタンスをContextオブジェクトに渡すだけで、処理を動的に切り替えることができます。
実際の実装
まずはAuthHelperクラスです。このクラスはStrategyクラスに相当します。
Strategyパターンではこのクラスを抽象クラスとして定義されます。
しかし、CakePHPのHelperクラスは親クラスのAppHelperクラスからの継承が必須であるため、ここではインターフェースとして定義しています。
これによって擬似的に抽象クラスの多重継承が実現します。
<?php interface AuthStrategyHelper { /* * ヘルパーで使いたいメソッドを抽象メソッドとして定義しておきます */ public function putUserName(); public function putUserCreated(); public function putSubTitle(); }
次はAuthStrategyHelperのサブクラスです。
今回はLoginHelperとLogoutHelperの2つがあります。
引用元の解説の通り、切り替えたい選択肢ごとにクラスを作成しています。
ここではログイン時とログアウト時がそれに当たります。
LoginHelper
<?php // コントローラで読み込んでいませんのでここでインポートします App::import('Helper', 'Authstrategy'); class LoginHelper extends AppHelper implements AuthStrategyHelper { //Authヘルパーから渡されたログインユーザのデータを保持するプロパティ private $user; /* * AuthStrategyHelperクラスで定義された抽象メソッド群を実装します */ public function putUserName() { return $this->user["Member"]["user_name"]; } public function putUserCreated() { return $this->user["Member"]["id"]; } public function putSubTitle() { return $this->putUserName() . "さん"; } /* * Authヘルパーからユーザのデータ渡してもらうためのSetメソッド */ public function setConponents($user) { $this->user = $user; } }
LogoutHelper
<?php // コントローラで読み込んでいませんのでここでインポートします App::import('Helper', 'Authstrategy'); class LogoutHelper extends AppHelper implements AuthStrategyHelper { /* * AuthStrategyHelperクラスで定義された抽象メソッド群を実装します */ public function putUserName() { return "ゲストさん"; } public function putUserCreated() { return "ログインすると表示されます"; } public function putSubTitle() { return "<a href='http://sigisi.sakura.ne.jp/cakephp/members/twitter'>Twitterからログイン</a>"; } }
さらに、Contextクラスに相当するクラス、AuthHelperクラスです。
<?php class AuthHelper extends AppHelper { //ヘルパーを読み込みます。 var $helpers = array("Login", "Logout"); //コンポーネントのインスタンスを保持するプロパティ private $conponents; /* * Viewの処理の前に実行されます。 * ViewインスタンスへAuthStrategyHelperクラスの子クラスのインスタンスを渡します。 */ public function beforeRender() { $view = ClassRegistry::getObject('view'); $this->conponents = $view->getConponents(); $view->Auth = $this->getInstance(); } /* * Sessionコンポーネントへの参照を使って認証を判定しています。 * 判定によって参照を返すヘルパーを変えています。 */ private function getInstance() { $user = $this->conponents["Session"]->read("Auth"); if(isset($user['Member']['user_id'])){ $this->Login->setConponents($this->conponents["Session"]->read("Auth")); return $this->Login; } else{ return $this->Logout; } } }
最後に、クライアント側のコードを見てください。
適当なViewの.ctpファイルです。
分り易くindex.ctpを使っています。
index.ctp
<div><?php echo $this->Auth->putUserName(); ?></div> <div><?php echo $this->Auth->putUserCreated(); ?></div> //結果 <div>Mr.HogeHoge</div> <div>2011-04-03 21:02:33</div>
引用
Strategyパターンのオブジェクト指向的要素
Strategyパターンは「継承」と「ポリモーフィズム」を活用しているパターンです。
StrategyクラスとConcreteStrategyクラスは、継承の関係にあります。親クラスであるStrategyクラスで処理内容が変わる部分を抽象メソッドとして定義します。一方、サブクラスであるConcreteStrategyクラスでは、抽象メソッドを実装し、具体的な処理を記述します。こうすることで、同じAPIを持ち、かつ具体的な処理が異なるクラス群を用意できます。
また、Contextクラスは、Strategy型のインスタンスを内部に保持します。このインスタンスは、具体的にはStrategyクラスを継承したサブクラスのインスタンスです。Contextクラスは、クライアントからの処理要求を受け取ると、保持したインスタンスに具体的な処理を委譲します。この時、処理を委譲する部分を、処理側の親クラスであるStrategyクラスのAPIだけを使ってプログラミングを行っておくことがポイントです。こうすることで、Strategy型のインスタンスがどの様な処理を行うものであれ、正しく動作することになります。
この結果、ConcreteStrategyクラスを簡単に差し替えたり、追加したりできるのです。Strategyパターンは、委譲を使って処理内容全体を切り替えるパターンと言えます。
なお、このような処理を切り替えるパターンとしては、Strategyパターン以外にTemplate Methodパターンがあります。Template Methodパターンでは、継承を使って処理内容の一部を切り替えています。
いかがでしたでしょうか
CakePHPのヘルパーというフレームワーク上の制約がある中で、出来る限りオブジェクト指向な設計にしてみました。
後半からは、ほとんど意地になって「Strategyパターン」を墨守した書き方をしています。
ソースからそんな気持ちが出てるかもしれません。
とはいえ、やはり汚いコードはプログラマの恥です。
悪い設計はそれ自体がバグです。
ソフトフェアは保守性そのものといえます。
プログラマは時間や技術力を言い訳にせず良い設計、美しいコードにするよう精進すべきであると考えています。
※この実装はCakePHP1.3でヘルパーからコンポーネントを使えるようにする方法に依存しています。