單例模式是最基礎也最常用的建構型設計模式,確保類別在全域只有唯一實例,並提供全域存取點。本文以資料庫連線類別為例,完整展示 PHP 單例實作細節,並逐步拆解「為什麼這樣寫」的核心邏輯。
<?php
class EasyFramework_Easy_Mysql {
// 靜態私有屬性:保存唯一實例
protected static $_instance = null;
// 私有建構子:禁止外部 new
private function __construct() {
// 實際連線邏輯:new PDO(...)
}
// 禁止克隆
protected function __clone() {}
// 禁止反序列化(PHP 7.1+)
public function __wakeup() {
throw new \Exception('Cannot unserialize singleton');
}
// 公開靜態方法:取得唯一實例
public static function getInstance() {
if (self::$_instance === null) {
self::$_instance = new self();
}
return self::$_instance;
}
}
// 使用方式
$db = EasyFramework_Easy_Mysql::getInstance();
var_dump($db);
?>
private function __construct() 讓外部無法 new Class(),掌握實例化控制權。protected static $_instance = null 以類別層級儲存唯一實例,不屬於任何物件。public static function getInstance() 提供全域唯一存取點,呼叫方式 Class::getInstance()。$_instance === null 為真,建立實例並賦值;後續呼叫直接回傳既有實例。__clone() 設為 protected/private,避免 clone $instance 產生副本。__wakeup() 拋出例外,防止 unserialize() 繞過建構子重建實例。| 誤區 | 說明 | 正確做法 |
|---|---|---|
| 濫用單例當全域變數 | 任何需要共享狀態的地方都用單例,導致耦合度極高、難以測試 | 依賴注入(DI 容器)管理共享服務,單例僅用於真正需要「全程唯一」的資源 |
| 單例類別過於龐大 | 把所有功能塞進單例,違反單一職責 | 拆分為多個單一職責服務,由 DI 容器管理生命週期 |
| 忽略 __clone / __wakeup | 可被克隆或反序列化產生多實例 | 務必宣告並拋出例外 |
| 建構子執行重度邏輯 | 連線失敗拋出例外導致單例永遠無法取得 | 建構子僅初始化屬性,連線放在 connect() 方法延遲執行 |
Laravel、Symfony 等框架透過 服務容器 綁定 singleton,實際上是「容器管理的單例」而非類別自行實作:
<?php
// Laravel 服務提供者中
$this->app->singleton('db', function ($app) {
return new DatabaseConnection($app['config']['database']);
});
// 任何地方解析
$db = app('db'); // 永遠回傳同一實例
?>
這樣既保有單例優勢,又維持可測試性(測試時可綁定 mock 物件)。
單例模式核心在於「私有建構子 + 靜態實例屬性 + 靜態存取器」。理解其七大步驟邏輯,能幫你在適當場合正確應用,避免濫用導致的架構腐化。