以前我們講過php單態設計模式之單例模式的理解及單例模式(Singleton)的常見應用場景,現在我們在原來的基礎上總結一下。
單例模式,就是保持一個對象只存在一個實例,并且為該唯一實例提供一個全局訪問點(一般是一個靜態的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);
- } //開源軟件:Vevb.com
- }
- //日志調用
- $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 (emptyempty(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種情況,看是否真的有必要使用。
新聞熱點
疑難解答