什麼樣的應用情境,會希望某個類別在系統中只存在一份實例呢?通常?都是負責系統全面性工作的類別,例如工作排程器。排程器在產生之後,不斷地檢視系統中排程工作的設定,然後在發現需要執行的工作時,加以執行。

負責資源集中控管的類別,只能存在一份實例
這個排程器即所謂「系統中全面性工作的類別」。倘若工作排程器有兩個或甚至更多的實例,很容易會發生同一個工作被重複喚起並執行的情況。

這正好突顯出「系統中全面性工作的類別」具備資源集中控管的特性。我們會希望在此類別中,集中管理某些特定的資源,並於此處集中執行相關的限制。

只要稍具規模的軟體系統,大概都會有控制單一實例的需求。有時候,開發者並不完全只是想集中控制某些事情,也許只是發現,某個功能或需求,只需要一份實例即可。

例如,系統中有一個負責將郵遞區號對應至地區名,以及將地區名對應至郵遞區號的類別-AreaCodeMapper。AreaCodeMapper的運作,只是在物件被產生因而執行建構式時,從資料庫中讀取郵遞區號及地區名的對應關係,並且記錄這對應關係。而當用戶端程式碼查詢該物件時,便直接回傳對應結果。

即使允許AreaCodeMapper產生不同的實例,也不會產生負面的結果,但是,這樣的類別就算是不同的實例,其中所含的資訊,以及所提供的功能完全一模一樣,產生多份實例,只是多花費時間和資料庫連線資源,從資料庫重複的載入郵遞區號地區名對應表,同時花費記憶體空間罷了。

Singleton設計模式
這種僅會有單一實例的類別,正是設計模式中廣為人所知且廣為人所用的「Singleton」。以下是以Java寫成的一個Singleton的制式長相:


public class AreaCodeMapper
{
   private static AreaCodeMapper instance;
   public static AreaCodeMapper getInstance ()
   {
      if( instance == null )
           instance = new AreaCodeMapper();
      return instance;
}
public String mapAreaCodeToName(String areaCode)
{ // 略 …
}
public String mapAreaNameToCode(String areaName)
{ // 略 …
}
   public AreaCodeMapper()
   { // 連至資料庫,並載入郵遞區號地區名對應表
   }
}


Singleton類別會提供和getInstance()相同作用的靜態方法(method),使得用戶端可以直接以類別名稱指涉呼叫getInstance()後,得到Singleton類別的唯一類別。

從上述的程式碼中,可以明白當AreaCodeMapper 的getInstance()第一次被呼叫時,靜態的Instance變數為Null,因此便利用New語法產生AreaCodeMapper的物件。可是當getInstance()第二次被呼叫時,靜態的Instance變數已不為null,因此便不會再產生AreaCodeMapper的物件實例。

所以,透過上述的寫法,用戶端只需要呼叫AreaCodeMapper的getInstance(),皆可以無礙地取得唯一一份的實例。

上述這種作法稱為「延遲初始化(Lazy Initialization)」。採用延遲初始化的寫法,是因為應用時,如果沒有任何用戶端需要AreaCodeMapper的實例,就可以節省時間及資源,不必載入郵遞區號地區名對應表,也不需要浪費記憶體空間了。

杜絕用戶端自行穿越便道
上述的寫法,仍有兩個盲點需要改進。

首先,AreaCodeMapper的建構式宣告為Public,這意謂著倘若用戶端程式碼不透過getInstance()取得AreaCodeMapper的實例,而是自行利用new AreaCodeMapper()的方式產生實例,我們將完全無法限制。再者,考慮多執行緒的環境,倘若兩執行緒同時進入getInstance(),並同時間都通過了if( instance == null )的檢查,那麼便會發生產生兩份實例的情況。為此,我們可改寫如下:


public class AreaCodeMapper
{
   private static AreaCodeMapper instance;
   public static synchronized AreaCodeMapper getInstance ()
   {
      if( instance == null )
         instance = new AreaCodeMapper();
      return instance;
   }
   public String mapAreaCodeToName(String areaCode)
   { // 略 …
   }
   public String mapAreaNameToCode(String areaName)
   { // 略 …
   }
   private AreaCodeMapper()
   { // 連至資料庫,並載入郵遞區號地區名對應表
   }
}


將AreaCodeMapper建構式的存取權限改為Private,這是很少見的情況。因為這麼設定,代表只有AreaCodeMapper本身才能產生實例。其餘的類別,無論是否在同一個Package,有無繼承的關係,皆完全無法呼叫這個建構式。

這使我們可以確保一件事:只有在AreaCodeMapper類別中的Methods,例如getInstance()才能夠利用New產生AreaCodeMapper物件。也因此,杜絕了用戶端不經正門、自行穿越便道產生實例的可能性。

此外,為了避免多執行緒的彼此競爭,我為getInstance()加上synchronized修飾詞,這使得同時間僅會有單一執行緒進到getInstance()中,也就使得多執行緒重入而造成的問題,獲得解決。

避免使用者複製產生其他實例
最後,對於像Java這樣在根類別java.lang.Object定義物件的克隆(clone())函式的程式語言,還要進一步防止用戶端利用getInstance()取得物件實例後,再呼叫物件的clone()函式,進而複製出其他的實例。

我們可以覆寫clone(),使得clone()被呼叫時,擲出異常,阻止用戶端透過clone()函式產生出一份以上的物件實例:


public Object clone() throws CloneNotSupportedException
{
   throw new CloneNotSupportedException();
}


以上,便是我們在實作僅具備單一實例之類別,也就是Singleton時,最普遍會遭遇到的議題。

Singleton雖然簡單,卻是開發每個系統時,幾乎都一定會運用到的模式。在設計類別時,設計者應該檢視類別是否具備Singleton的需求,例如集中控管資源或共享資源,並且決定是否套用,使得該類別僅產生並維持一份實例。

《作者簡介》王建興
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC Game到P2P網路電話都在他的涉獵範圍之內。

相關閱讀:
探索產生物件的技巧(1)當心隨手New一下引發的衝擊效應
探索產生物件的技巧(2)生成模式的初階應用
探索產生物件的技巧(3)鬆綁程式內工廠與產品的關聯性
探索產生物件的技巧(4)抽象可抵擋變動引發的星星之火
探索產生物件的技巧(6)受夠重新開機?試試自行管理記憶體
探索產生物件的技巧(7)避免時效性不高的重複性動作
探索產生物件的技巧(終)間接產生物件的訴求:降低相依性

熱門新聞

Advertisement