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でヘルパーからコンポーネントを使えるようにする方法に依存しています。

※また、デザインパターンの説明はDoYouPHPさんからお借りしました。本当にありがとうございます。