1.單態設計模式含義:
單態模式的主要作用是保證在物件導向編程設計中,一個類只能有一個執行個體對象存在。作為對象的建立模式,單例模式確保某一個類只有一個執行個體,而且自行執行個體化並向整個系統全域地提供這個執行個體。它不會建立執行個體副本,而是會向單例類內部儲存的執行個體返回一個引用。
2.單台模式的三個關鍵點:
① 需要一個儲存類的唯一執行個體的靜態成員變數;
②建構函式和複製函數必須聲明為私人的,防止外部程式new類從而失去單例模式的意義;
③必須提供一個訪問這個執行個體的公用的靜態方法(通常為getInstance方法),從而返回唯一執行個體的一個引用 。
| 代碼如下 |
複製代碼 |
<?php class DB { private static $obj = null; //聲明一個私人的,靜態成員屬性$obj private function__construct() { //私人構造方法,只能在類的內部執行個體化對象 echo "串連資料庫成功<br>"; } public static function getInstance() { // 通過此靜態方法才能擷取本類的對象 if(is_null(self::$obj)) //如果本類中的$obj為空白,說明還沒有被執行個體化過 self::$obj = new self(); //執行個體化本類對象 return self::$obj; //返回本類的對象 } public function query($sql) { //執行SQL陳述式完成對資料庫的操作 echo $sql; } } $db = DB::getInstance(); //只能使用靜態方法getInstance()去擷取DB類的對象 $db -> query("select *from user"); //訪問對象中的成員 ?> |
單例模式,就是保持一個對象只存在一個執行個體。並且為該唯一執行個體提供一個全域訪問點(一般是一個靜態getInstance方法)。單例模式應用情境非常廣泛,例如:
資料庫操作對象
日誌寫入對象
全域配置解析對象等
這些情境的共同特徵是從商務邏輯上來看運行期間改對象卻是
只需要一個執行個體
不斷new多個執行個體會增加不必要的資源消耗
全域調用便利
。
下面分別說明這三個方面:
1. 業務上只需要一個執行個體
以資料庫連接對象為例,加入有一個購物網站,有一個MySQL資料庫
127.0.0.1:3306
,那麼在一個進程中無論我們需要進行多少次針對改資料庫的操作,都只需要串連資料庫一次,使用相同的資料庫連接控制代碼(MySQL Connection Resource),從業務需求上來看就只需要一個執行個體。
相反,同樣以購物網站為例,存在許多商品,這些商品都不一樣(id,name,price..),這個時候需要顯示一個商品列表,加入我們建立一個
Product
作為資料對應對象,那麼從業務需求上來說,一個執行個體就無法滿足業務需求,因為每個商品都不一樣。
2. 不斷new操作增加不必要的資源消耗
我們一般會在類的構造方法(new操作肯定會調用)中進行一些業務操作,例如資料庫連接對象可能會在構造方法中嘗試讀取資料庫配置並進行資料庫連接(如mysqli::__construct())、日誌寫入對象會判斷日誌寫入目錄是否存在並寫入(不存在可能嘗試建立改目錄)、全域配置解析對象可能需要定位設定檔的儲存目錄並進行檔案掃描等。
這些業務都會消耗相當的資源,如果在一個進程中我們值需要做一次,將會非常有利於我們提高應用的運行效率。
3. 全域調用便利
因為單例模式的一大特點就是通過靜態方法擷取對象執行個體,那麼就意味著訪問對象的方法時不需要先new一個對象的執行個體,如果改對象需要很多地方使用,則提高了調用的便利性。
通過一個日誌操作類來舉例:
| 代碼如下 |
複製代碼 |
class Logger{
//首先,需要一個私人的靜態變數來儲存產生的對象執行個體 private static $instance;
//業務變數,儲存日誌寫入路徑 private $logDir;
//構造方法,注意必須也是私人的,不允許被外部執行個體化(即在外部被new) private function __construct(){ //調試輸出,測試對象被new的次數 echo "new Logger instance rn"; $logDir = sys_get_temp_dir(). DIRECTORY_SEPARATOR . "logs"; if(!is_dir($logDir) || !file_exists($logDir)){ @mkdir($logDir); } $this->logDir = $logDir;
}
//類唯一執行個體的全域訪問點,用於判斷並返回對象執行個體,供外部調用 public static function getInstance(){ if(is_null(self::$instance)){ $class = __CLASS__; //擷取本對象的類型,也可以用new self()方式 self::$instance = new $class(); } return self::$instance; }
//重載__clone()方法,不允許對象對複製 public function __clone(){ throw new Exception("Singleton Class Can Not Be Cloned"); }
//具體的業務方法,實際可以有很多方法 public function logError($message){ $logFile = $this->logDir . DIRECTORY_SEPARATOR . "error.log"; error_log($message, 3, $logFile); } }
//日誌調用 $logger = Logger::getInstance(); $logger->logError("An error occured"); $logger->logError("Another error occured");
//或者這樣調用 Logger::getInstance()->logError("Still have error"); Logger::getInstance()->logError("I should fix it"); |
在單例模式中可能遇到一種比較特殊的情況,比如資料庫連接對象,對於大型應用來說,很可能需要串連多台資料庫,那麼不同的資料庫公用一個對象可能會產生問題,比如串連的分配、擷取
insert_id
,
last_error
等。這個問題也比較好解決,就是把我們的$instance變數變成一個關聯陣列,通過給getInstance方法傳入不同的參數擷取不同的"單例對象"(引號的含義是:嚴格來說類可能被new多次,但是這個new也是在我們的控制之內的,而不是在類外部):
| 代碼如下 |
複製代碼 |
class MysqlServer{ //注意,變成複數了哦^_^ 當然只是為了標識而已 private static $instances = array();
//業務變數,保持當前執行個體的mysqli對象 private $conn;
//顯著特徵:私人的構造方法,避免在類外部被執行個體化 private function __construct($host, $username, $password, $dbname, $port){ $this->conn = new mysqli($host, $username, $password, $dbname, $port); }
//類唯一執行個體的全域訪問點 public static function getInstance($host='localhost', $username='root', $password='123456', $dbname='mydb', $port='3306'){ $key = "{$host}:{$port}:{$username}:{$dbname}"; if (empty(self::$instances[$key])){ //這裡也可以用 new self(); 的方式 $class = __CLASS__; self::$instances[$key] = new $class($host, $username, $password, $dbname, $port); } return self::$instances[$key]; }
//重載__clone方法,不允許對象執行個體被複製 public function __clone(){ throw new Exception("Singleton Class Can Not Be Cloned"); }
//查詢業務方法,後面省略其它業務方法 public function query($sql){ return $this->conn->query($sql); }
//儘早釋放資源 public function __destruct(){ $this->conn->close(); } } |
問題1:單例類能否擁有子類,因為單例類的構造方法是私人的,因此無法被繼承,如果要繼承則需要將構造方法改為protected或public,這就違背了單例模式的本意。因此,如果你想給單例類加子類,那就需要回頭想想是否錯用了模式,或者結構設計上有問題。
問題2:單例濫用,單例模式相對來說比較好理解和實現,因此一旦認識到單例模式的好處,很可能什麼類都想寫成單例,因此在使用次模式之前一定要考慮上述3種情況,看是否真的有必要使用。