PHPでシングルトン(Singleton)を学ぶ【デザインパターン】

PHP応用 デザインパターン PHP
※当サイトはアフィリエイト広告を掲載しています。

デザインパターンは、コードの保守や拡張性を高めるための定番として、全23の手法があります。

その中の一つがシングルトンパターン(Singleton Pattern)です。内容は、特定のクラスのインスタンスが1つしか生成されないよう保証するというもの。比較的よく用いられるパターンと言えるでしょう。

本ページではPHPを利用して、シングルトンの基本的な概念~実装方法についてわかりやすく解説します。

シングルトンパターンとは?

概要

最初にも触れたとおり、特定のクラスのインスタンスが1つしか生成されない(存在しない)ことを保証するためのパターンです。

「Singleton」という言葉は、英語で「単一のもの」という意味を持っています。なのでその名の通りのパターンということになりますね。

このパターンは、以下の様な場合に有効です。

  • 動作を軽くする等のためにインスタンスを再利用したい場合。
  • 特定のリソースやサービスに対応する1つのインスタンスを共有してアクセスしたい場合

……これだけではまだよく分からないと思いますが、後にコード例を挙げますので安心してください。

その実装方法も、

  • プライベートなコンストラクタを作る
  • そのクラスの唯一のインスタンスを返す静的メソッドを作る

という感じで行います。

基本概念はこれだけ。

デザインパターンは難しいイメージがありますが、比較的簡単な方と言えるでしょう。

Lara
Lara

実は私も最初に使いこなせるようになったのが、このシングルトンパターンです。それでは具体的に解説していきましょう!

PHPでのシングルトンパターンの基本的な実装

基本的なシングルトンクラスの作成

シングルトンパターンをPHPにて簡素に実装した例です。

class Singleton {
    private static $instance = null;

    private function __construct() {}

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

    private function __clone() {}

    private function __wakeup() {}
}

概要で触れたとおり、このコードには以下の様な特徴があります。

  • コンストラクタがprivateに指定されている
  • getInstance()という、インスタンスを返す静的メソッドがある
  • __clone(), __wake()private指定にしている

これらを順に説明していきましょう。

コンストラクタをprivateにする理由

コンストラクタの多くはpublicで指定しますよね。そうすることで、外部からnewして使用することができます。

class PublicConstructorClass {
    public function __construct() {
        // ...
    }
}

// クラスの外部からインスタンスを作成する
$instance = new PublicConstructorClass(); // 問題なく動作する

対してコンストラクタをprivate指定すると、外部からnew出来ません。自身(内部)のメソッドからしかnew出来なくなります。

class PrivateConstructorClass {
    public function __construct() {
        // ...
    }
     public static function createInstance() {
        // このメソッド経由ならnewできる
        return new self(); 
    }
}

// クラスの外部からインスタンスを作成する
$instance = new PrivateConstructorClass(); // newできない!

getInstance() メソッドの役割

このままだと外部からnewできないので、内部でnewする役割を持つのがgetInstance()メソッドの役割です。

もしもself::$instanceが無ければ作成&変数に保持、という前処理をした上でインスタンスを返しています。

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

これにより、初回のみインスタンスを作成。2度目以降の呼び出しではインスタンスを作成しません。

self::$instanceはもちろんprivate指定しておきますので、外部からはアクセス不能です。

これでインスタンスは1つだけ、というのが保証された!……ように見えますが、実は抜け道があります。

余談ですが、getInstance()ではないメソッド名をつけても、問題無く動作します。しかしながらこの名称が通例ですので、誰が見ても「シングルトンパターンなんだな」と分かるためにもgetInstance()を使うのが良いでしょう。

__clone() と __wakeup() をprivateにする理由

実は、PHPのマジックメソッドを使うことで、「インスタンスは1つ」という原則を破ることが可能です。

その一つは__clone()メソッド。インスタンスのクローンを作成するマジックメソッドです。

以下の様にすることで、異なるインスタンスが出来てしまいます。

class Singleton {
    private static $instance;

    private function __construct() {
        // ...
    }

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

// Singletonのインスタンスを取得
$original = Singleton::getInstance();

// クローンを作成
$cloned = clone $original;

// 結果の確認
if ($original === $cloned) {
    echo "同じインスタンスです";
} else {
    echo "異なるインスタンスです";  // このメッセージが出力される
}

__wakeup()メソッドはアンシリアライズ(シリアル化された変数を戻す)際に使われます。

こちらも同様に、インスタンスが複製されてしまいます。

class Singleton {
    private static $instance;

    private function __construct() {
        // ...
    }

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

// Singletonのインスタンスを取得
$original = Singleton::getInstance();

// シリアライズ
$serialized = serialize($original);

// アンシリアライズして新しいインスタンスを作成
$unserialized = unserialize($serialized);

// 結果の確認
if ($original === $unserialized) {
    echo "同じインスタンスです";
} else {
    echo "異なるインスタンスです";  // このメッセージが出力される
}

これらのメソッドを外部から呼べなくするためにprivateに指定しています。

自分のシステムなら分かっているので必須ではありませんが、複数人が関わるプロジェクトであれば念のためにやっておくのが良いでしょう。

これで「インスタンスは一つ」が完成!

要点の説明が済んだので、先ほどのクラスを使い、シングルトンパターンが正しく実装されていることを確認してみます。

以下が簡単な使用例です。

$instance1 = Singleton::getInstance();
$instance2 = Singleton::getInstance();

var_dump($instance1 === $instance2);  // bool(true)

getInstance()を何度呼び出しても、同じインスタンスが返ります。

この場合Singletonクラスのインスタンスは、動作しているスクリプトの中では1つだけと保証されていると言えます。

Lara
Lara

でもインスタンスが1つだとなにが良いのだろう……と思う方も多いと思います。これから活用例と供に解説していきましょう。

シングルトンの活用例: データベース接続

シングルトンパターンは、特にデータベース接続においてその効果を発揮します。

というのもデータベースへの接続は、比較的リソースを消費する重い操作です。無駄に多数の接続を生成・維持するのは効率的ではありません。

そんな時「インスタンスは一つだけ」のシングルトンパターンを実装すれば、無理なく問題が解決します。

ここでは実際のコード例を見ながら、そのメリットについて見ていきましょう。

データベース接続用のシングルトンクラスの実装

以下は、PHPにもともと備わっているPDOを用いての、データベース接続用のシングルトンクラスです。

class DatabaseConnection {
    private static $instance = null;
    private $connection;

    private function __construct() {
        $dsn = "mysql:host=localhost;dbname=mydatabase";
        $username = "username";
        $password = "password";
        $this->connection = new PDO($dsn, $username, $password);
    }

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

    public function getConnection() {
        return $this->connection;
    }

    private function __clone() {}

    private function __wakeup() {}
}

コンストラクタでPDOでのデータベース接続&それを保持するプロパティがあります。それ以外は最初の例とほとんど同じです。

このクラスを使用すれば、インスタンスは1つだけ=データベースへの接続は1つだけなのが保証されます。

使ってみる

このクラスを実際に使ってみましょう。

以下は、ユーザーや注文を取得するようなイメージのコード例です。

class User {
    public static function getAllUsers() {
        $dbConnection = DatabaseConnection::getInstance();
        $pdo = $dbConnection->getConnection();
        $stmt = $pdo->query("SELECT * FROM users");
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

class Orders {
    public static function getOrdersForUser($userId) {
        $dbConnection = DatabaseConnection::getInstance();
        $pdo = $dbConnection->getConnection();
        $stmt = $pdo->prepare("SELECT * FROM orders WHERE user_id = :user_id");
        $stmt->bindParam(':user_id', $userId);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

// 使用例
$users = User::getAllUsers();
foreach ($users as $user) {
    echo $user['id'] . ": " . $user['name'] . "<br>";
}

$orders = Orders::getOrdersForUser(1);
foreach ($orders as $order) {
    echo "Order for User ID 1: " . $order['order_number'] . "<br>";
}

UserクラスでもOrdersクラスでも、一つの同じインスタンスから、同じDBのコネクションを使用できるというわけです。

何も考えなくても、同じインスタンスの使用が担保されるのが良いですね。

データベース接続でシングルトンを利用するメリット

このケースでのメリットは以下の3つに分けられます。

  1. リソースの節約
    データベースへの接続は、その都度リソースを消費します。シングルトンを使用することで、一度の接続をアプリケーション全体で再利用することができ、リソースの無駄を避けることができます。
  2. 接続の一貫性
    アプリケーション全体で1つの接続を共有することで、トランザクション管理などの際に一貫性を持たせやすくなります。異なる接続を使用すると、コミットやロールバックが想定外の挙動をとる可能性があるからです。
  3. 設定の一元管理
    シングルトンクラス内に接続情報(ホスト名、ユーザ名、パスワードなど)を記述することで、設定の変更や管理が容易になります。

先ほどの利用例はコードが短かったので、あまりメリットには感じづらいです。しかし実際のコードは多数のファイルを読み込むなど、もっと複雑です。

そうなった際にこれら3つがメリットとして活きてくるのです。

そのクラスのインスタンスは1つだけ。データベースの接続は、このシングルトンパターンが比較的向いている利用例だと言えるでしょう。

Lara
Lara

現在はMySQL固定ですが、他のDBに対応出来るような改良もできると思います。その場合は別のデザインパターンも使えそうです。それはまた別の機会に!

シングルトンの活用例: 設定管理

多くのアプリケーションには設定ファイルが存在します。

例えばデータベースのアカウント情報やAPIキー、各種見栄え・振る舞いに関するパラメータ等など、様々な設定をします。

この設定ファイルを読み取り、通常はアプリケーション全体で使用します。そのため簡単に管理・使用できると便利です。

そこで、シングルトンパターンでの実装を考えてみましょう。

設定管理用のシングルトンクラス

以下は、設定管理のためのシングルトンクラスの例です。

class AppConfig {
    private static $instance;
    private $settings = [];

    private function __construct() {
        // 例として、ここで設定ファイルを読み込む
        $this->settings = parse_ini_file('config.ini');
    }

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

    public function getSetting($key) {
        return isset($this->settings[$key]) ? $this->settings[$key] : null;
    }
}

$config = AppConfig::getInstance();
$dbUser = $config->getSetting('DB_USER');

7行目のparse_ini_file('config.ini');で、設定ファイルを読み込み、保持しています。

設定ファイルはDB接続ほどの負荷ではないかもしれませんが、何度も読み込む必要はありませんよね。

シングルトンパターンを利用することで、設定を1度の読み込みに済ませることが可能です。

値を変更できるようにするには注意

このクラスはiniファイルから設定を取得するだけで、値を後からセットすることはできません。

例えば以下の様なメソッドを作ればセット機能は簡単に追加できます。

    public function setSetting($key, $value) {
        $this->settings[$key] = $value;
    }

ダメとは言いませんが、値を変更できる場合は気をつける必要があります。

というのもシングルトンのインスタンスは、グローバルなインスタンスのように使えるからです。

この設定値がアプリケーションの多くの部分で使用され、変更もされるということ。それは他の部分にも影響を与えるリスクとなります。

場合によっては便利ですが、メンテナンスや拡張性に問題をもたらす可能性も認識しておく必要があります。

これに関しては改めて後述します。

値が変えられる=要はグローバル変数と同じような問題が起こってしまうというわけですね。

シングルトンの活用例: ログ管理

Webアプリケーションでは、エラーを始めとするさまざまな情報をログとして記録するのが一般的です。

これらのログ情報は、問題の発見やその原因を特定するための手がかりとなります。

逆に無いと、問題に気づくことすら出来ませんから重要です。

Lara
Lara

実際には他者の作ったログ機能が無いWebアプリケーションもたまに見ます。。

ログ管理用のシングルトンクラス

ログはアプリケーション全体で利用するわけですから、シングルトンパターンは比較的向いています。

以下は、ログ管理のためのシングルトンクラスで実装した例です。

class Logger {
    private static $instance;
    private $handle;

    private function __construct() {
        $this->handle = fopen('app.log', 'a');
    }

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

    public function log($message) {
        fwrite($this->handle, date('Y-m-d H:i:s') . ': ' . $message . "\n");
    }
}

$logger = Logger::getInstance();
$logger->log('This is a log message.');

少し変えただけなので、説明は不要ですね。

先ほどの設定ファイルを読み込む部分が、ログファイルのオープンに変わっただけです。

なので理解のダメ押し用のコード例と思ってください。

シングルトンの解説用につき、ファイルのクローズ処理は書いていません。そこまで考えると、実際に使うにはもっと良いやり方がありそうですね。

シングルトンパターンの注意点

一通り例を挙げたところで、メリットは理解できた方が多いと思います。

一方でデメリットや注意点については分かりづらいものがあります。

ここではシングルトンパターンの注意点について考えてみましょう。

値の変更が入る場合

シングルトンパターンのインスタンスは、アプリケーションのどこからでもアクセスできます。これはとても便利ですよね。

しかしながら先ほどの活用例でも挙げたように、そのインスタンスが変更可能な場合は注意が必要です。

一部が変更されると、それに関連する他の部分にも影響をもたらす可能性がるからです。

具体的に挙げてみましょう。

class AppConfig {
    private static $instance;
    private $config = [];

    private function __construct() {}

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

    public function setConfig($key, $value) {
        $this->config[$key] = $value;
    }

    public function getConfig($key) {
        return $this->config[$key] ?? null;
    }
}

このシングルトンを使って、アプリケーションの別の場所で設定を変更することを考えます。

// 初期設定
AppConfig::getInstance()->setConfig('app_mode', 'production');

// 何らかの処理の中で設定を変更
function someFunction() {
    // ~中略~
    AppConfig::getInstance()->setConfig('app_mode', 'debug');
    // ~中略~
}

// ~中略~
someFunction();
// ~中略~

// 他の場所で設定を取得
// こっそり変えられていたら意図しない動作に……
$mode = AppConfig::getInstance()->getConfig('app_mode'); 

上記のコードでは、アプリケーションの一部で設定(app_mode)を変更しています。

これにより、someFunction 関数の実行後に設定を取得すると、期待しない値が返される可能性があります。

このように、シングルトンのインスタンスが変更可能な場合、その状態を予測するのが難しくなります。

とは言え実際問題、小規模なアプリケーションでは問題にならないことも多いでしょう。

しかしプロのエンジニアとして、規模の大きいアプリケーションを開発する際は注意が必要です。

テストの難しさ

グローバルな状態を持つシングルトンは、ユニットテストを行う際に問題を引き起こすことがあります。

特に前述のように、値の変更が入る場合は想像に難くないことでしょう。

また、インスタンスをリセットする方法がない場合、テストの前後で状態を初期化することもできない問題が生じます。

値の変更が無い場合でも、シングルトンは新しいインスタンスを作成できません。そのため“シングルトンならでは”の問題が発生することもあります。

だからシングルトンはダメ、というわけではありません。適材適所で使うことが大事です。

依存関係の増加

使い方によっては、依存関係がいたるところで増加するという問題もあります。

class Configuration {
    private static $instance;
    private $settings = [];

    private function __construct() {}

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

    public function setSetting($key, $value) {
        $this->settings[$key] = $value;
    }

    public function getSetting($key) {
        return $this->settings[$key];
    }
}

上記のクラスを利用してみます。

class User {
    public function getLocale() {
        // Configurationのシングルトンに依存
        return Configuration::getInstance()->getSetting('locale');
    }
}

class Theme {
    public function getColorScheme() {
        // 同じくConfigurationのシングルトンに依存
        return Configuration::getInstance()->getSetting('color_scheme');
    }
}

このコード例では、多くのクラスやコンポーネントが設定を利用すると想定されます。

それはつまり、多くの異なるクラスが「依存」しているということ。

もしConfiguration クラスに変更を加えたとすると、そのすべての依存関係も影響を受ける可能性があります。

たとえば、getSetting メソッドの仕様を変更すれば、User, Theme などのクラスも変更を加える必要が生じます。

このような強い結合性は、大抵の小さなWebアプリケーションでは問題にはなりません。

しかし大きなコードベースでは特にメンテナンスが困難になり、未来の拡張や変更に問題が生じることがあります。

まとめ

以上、シングルトンパターンについて説明しました。

そのクラスのインスタンスは1つだけ。それが保証されることで非常に便利に使えるシーンがあります。

本文で書いたとおり、シングルトンはデザインパターンの中で比較的簡単に使えます。メリットが得られる場面では、ぜひ使ってみてください。

ただ、覚えると何でもかんでも使いたくなるのがデザインパターンの悪いところです(汗)。適切な場所で使うことを第一に考える必要があるでしょう。

コメント

タイトルとURLをコピーしました