1. Home
  2. PHP
  3. DesignPatterns
  4. Singletonパターン | PHPデザインパターン

Singletonパターン | PHPデザインパターン

  • 公開日
  • カテゴリ:DesignPatterns
  • タグ:PHP,DI,DesignPatterns,Creation,Singleton
Singletonパターン | PHPデザインパターン

Singleton パターンは、インスタンスの生成制御に関するデザインパターン手法の1つで、インスタンスが1つしかない事を保証します。

この、「インスタンスが1つしかない事を保証する」とはどういう事なのか。

Singleton パターンが用いられるのは以下のような状況の時です。

  • そのオブジェクト(クラス)を毎回インスタンス化したとして、複製されたそれらのどれかの状態や値が変更された際に、毎回必ず全てのインスタンスの保持する状態が同一でなければならない状況の時。状態の同期を実装するくらいなら、初めからそのインスタンスが単一であった方が結局シンプルである。
  • インスタンスの生成自体にも負荷がかかるので、1つで済む役割のオブジェクトならそもそも1つだけ存在できれば良い。
  • そのオブジェクトが保有(もしくはロード)する何等かのデータの量がとてつもなく多い場合、いくつもインスタンスが生成されるとパフォーマンスの低下・障害に通じる。

なんだかんだ言ってもあれこれすればインスタンスを1つに限定しなくてもなんとかなる場合もあったりしますが、結局は無駄に複雑化させる事なく、極力シンプルにアプリケーションを構築する為には、Singleton パターンを採用するに至ります。

また、こうした「よからぬ状況」の回避を運用ではなく動作ロジックとして完全に単一である事を実現させる事で、そのインスタンスが1つのみである事を必ず保証し、運用どころか開発時点から事故を無くします。

Singleton パターンのクラス図は以下のようになります。

その名の通り1つのクラスのみ。極めてシンプルです。

実装例

Singleton パターンを実装したコードは以下のようになります。

app/Singleton/SampleSingleton.php
<?php

namespace App\Singleton;

class SampleSingleton
{
    private static $instance;

    private $id;    // 同一インスタンス検証用のID

    private function __construct()
    {
        $this->id = hash('sha256', time());
    }

    public static function getInstance()
    {
        if (empty(self::$instance)) {
            self::$instance = new SampleSingleton();
        }
        return self::$instance;
    }

    public function getId()
    {
        return $this->id;
    }

    public final function __clone()
    {
        throw new \Exception('This Instance is Not Clone');
    }

    public final function __wakeup()
    {
        throw new \Exception('This Instance is Not unserialize');
    }
}

上記のコードは最小構成です。実際にはシングルトンを実現した上で、必要なふるまいを定義していきます。

という事で、このクラスが提供している機能は唯一「ID を返す」という事になります。

上記のように動的に生成された ID が、必ず同じ値を返す為には、やはり単一インスタンス、つまりはシングルトンである必要があります。新しいインスタンスを生成すると新しい ID が生成されてしまうからです。

private function __construct()
{
    $this->id = hash('sha256', time());
}

コンストラクタのアクセス修飾子を private にする事で、new 演算子を用いて新たなインスタンスを作成できなくさせています。

このオブジェクトにアクセスするには、そのオブジェエクトが持つ提供メソッドから取得する必要があります。それが getInstance() メソッドです。

public static function getInstance()
{
    if (empty(self::$instance)) {
        self::$instance = new SampleSingleton();
    }
    return self::$instance;
}

インスタンスが生成されていなければ自身の生成を行い、1度生成されてからはそれのみを返却します。

そしてこのインスタンスに静的にアクセスできるように、getInstance() とメンバ変数 $instancestatic で定義します。いわゆる遅延静的束縛 (Late Static Bindings) です。

private static $instance;

ちなみに、これだけではインスタンスの単一性を保証する事ができません。 PHP に限りませんが、インスタンスを複製する方法などあまたあるわけで、とはいえ非常識的な部分は置いておくとして、マジックメソッドに関する生成複製部分は回避しておく必要があります。

そこで以下です。

public final function __clone()
{
    throw new \Exception('This Instance is Not Clone');
}

public final function __wakeup()
{
    throw new \Exception('This Instance is Not unserialize');
}

マジックメソッドである __clone()__wakeup() は、アクセス修飾子を private にするのでも良いですが、ここでは例外を投げています。(どっちでも良いです)つまりは、

  • clone 演算子でインスタンスをクローンさせない
  • グローバル関数の unserialize() でインスタンスをアンシリアライズさせない

という事を行っています。そして最後に

public function getId()
{
    return $this->id;
}

このクラスの唯一の機能である、ID を返すメソッドです。

ちなみに返却する ID ですが、インスタンスを1つだけ生成する際にコンストラクタで ID の生成を行っています。

$this->id = hash('sha256', time());

ではクライアント側からこのクラスを利用します。

<?php

use App\Singleton\SampleSingleton;

$singleton = SampleSingleton::getInstance();

echo $singleton->getId();

$singleton2 = SampleSingleton::getInstance();

echo $singleton2->getId();

結果は以下になります。

5a7f72e01c129081cc23ca2196a76e19de7051341c3b0ffc7d3baba759b15595

5a7f72e01c129081cc23ca2196a76e19de7051341c3b0ffc7d3baba759b15595

同じ ID が返されている事が確認できます。インスタンスを生成する毎に ID が生成されるので、この2つが同じ ID ということは、同一のインスタンスである事が確認できます。

そして、new も clone もできません。

$new = new SampleSingleton();   // ×

$clone = clone $singleton;      // ×

まとめ

Singleton パターンはインスタンスが1つしかない事を保証するものですが、それによってインスタンスの生成を制御し、アプリケーションをシンプルに、そして堅牢に保つ為の手法です。

Laravel などの PHP フレームワークを使っているなら、コンテナとかキューの処理なんかをイメージするとより理解しやすいかもしれません。例えばキューイングは処理の順番を把握し処理したいのでインスタンスは1つが良い。そういった意味では、DI コンテナなんかは、事実上シングルトンを提供しているようなものです。

ただ反面、何も考えずに用いるとアンチパターンになってしまう事も多々あり、シンプルな構造とは言えその本質を良く理解してからの使用検討が良い手法の1つとも言えます。(密結合/グローバル変数のあれこれ/メンテナンス性の低下など)

まあ依存性注入 (Dependency Injection) がありますしね。

とは言わず、先人の知恵の一つとして、そして己の手数の一つとして理解しておくと良いと思います。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級