Singletonパターンの抽象化クラスを作る(PHP 5.3以上の話)

シングルトンパターンでクラスを作成するとき、getInstanceなるメソッドを毎度毎度各クラスに作っていたのですが、これを何とか抽象化してみようとした話。

一般的なシングルトンパターン(おさらいも兼ねて)

たぶん、こんな感じ。

class Singleton
{
    private static $_instance = null;

    public static function getInstance()
    {
        if(is_null(self::$_instance))
        {
            self::$_instance = new self();
        }
        return self::$_instance;
    }
    
    // --- 以下このクラスのなんか処理 --- //
}

で、呼び出すときはこんな感じ。

$test = Singleton::getInstance();

これで変数「$test」は「Singleton」クラスのインスタンスを持つわけです。
そして静的な変数でクラス自信が自分のインスタンスを持ち「getInstance」メソッドでそれを取得することにより、クラスの実態を1つだけに制限しておくことができるんですね。

このあたりはコンストラクタを作って「new」したり「getInstance」したりして検証してみてください。

で、何が嬉しいのかっていうと、例えばデータベースへの接続クラスなどに流用すると、一度接続したリソースがそのままそのスクリプト処理内で使い回せるわけです。newする度に接続を張っているとメモリを無駄に消費したり、DB側で「max connection」エラーになったりします。

グローバル変数かのごとく使う人もいますが、まぁ、そいうこともできますわな。

抽象化への道のり

ここで実体を1つに制限したいクラスすべてに「getInstance」メソッドを記述するのは面倒だと感じ、抽象化クラスを作ってみようと思うのは通常の思考回路です。

早速やってみました。

abstract class Singleton
{
    protected static $_instance = null;

    public static function getInstance()
    {
        if(is_null(self::$_instance))
        {
            self::$_instance = new self();
        }
        return self::$_instance;
    }
}

できました。「abstract」付けて「$_instance」をprivate → protectedに変更しただけです。
ツッコミはまだ待ってください。
できると思っていたんです。少なくともこの瞬間は。

検証はこんな感じで実行

class Test extends Singleton
{
}

$test = Test::getInstance();
実行結果
Fatal error: Cannot instantiate abstract class Singleton in ~

抽象化クラスのインスタンスは作成できないよってエラーが。うーん。
「self」を指定した時は継承先のクラスではなく継承元のクラスが指定されてしまうようです。

PHP5.3以降では遅延静的束縛と言う機能が

php5.3以降では遅延静的束縛という機能があるようで、これで解決できるようです。
本番の環境はphp5.1なのですが、php5.3の環境で興味本位で作って動かしてみます。

abstract class Singleton
{
    protected static $_instance = null;

    public static function getInstance()
    {
        if(is_null(self::$_instance))
        {
            self::$_instance = new static();
        }
        return self::$_instance;
    }
}

「new self()」の部分を「new static()」に変えただけ。これでエラーなく動きました。
PHP5.3すげー!早くphp5.3以降に移行したいぞっ!!

次は複数クラスから継承して確認。

class Test1 extends Singleton
{
    public function say()
    {
        echo "Test1\n";
    }
}

class Test2 extends Singleton
{
    public function say()
    {
        echo "Test2\n";
    }
}

$test1 = Test1::getInstance();
$test2 = Test2::getInstance();

$test1->say();
$test2->say();
実行結果
Test1
Test1

あるぇー!?なんかうまく動かない。

って、抽象化クラスのメンバ変数の「$_instance」が静的(実態が一つ)なんだから当たり前か。

ではどうするかって「$_instance」を配列にして、クラス名をキーにして都度インスタンスを突っ込んでやることにしました。

abstract class Singleton
{
    protected static $_instance = array();

    public static function getInstance()
    {
        $key = get_called_class();
        if(!isset(self::$_instance[$key]))
        {
            self::$_instance[$key] = new static();
        }
        return self::$_instance[$key];
    }
}

「get_called_class」で呼び出したクラス名が取得できますのでこれをキーにしています。これも遅延静的束縛の機能みたいですね。

class Test1 extends Singleton
{
    public function say()
    {
        echo "Test1\n";
    }
}

class Test2 extends Singleton
{
    public function say()
    {
        echo "Test2\n";
    }
}

$test1 = Test1::getInstance();
$test2 = Test2::getInstance();

$test1->say();
$test2->say();
実行結果
Test1
Test2

結果PHP5.1じゃ使えないけど

まぁ、満足。そんな増えるもんじゃないし、現行は都度クラスに書いて頑張る。

ちなみに

今回は書いてなかったけど、シングルトンパターンではコンストラクタをプライベートメソッドにして他から呼び出せないようにしたり、「__clone」メソッドをオーバーライドしてオブジェクトを複製させないようにするのが定石の様ですね。

abstract class Singleton
{
    protected static $_instance = array();

    protected function __construct()
    {
        /* privateが定石と言いながら抽象化クラスにしてしまったので、
         * protectedがよろしいかと。
         * もちろん継承先でもprotectedで
         */
    }

    public static function getInstance()
    {
        $key = get_called_class();
        if(!isset(self::$_instance[$key]))
        {
            self::$_instance[$key] = new static();
        }
        return self::$_instance[$key];
    }
    
    final public function __clone()
    {
        /* __cloneメソッドは「clone $obj;」で呼び出されます。
         * オーバーライドして何もしなくても結果複製されてしまうので
         * 例外を投げるのがいいでしょう。
         */
        throw new Exception('複製しないで');
    }
}

コメントを残す