獨體模式(Singleton Pattern)

獨體模式(Sigleton Pattern)

以下定義與心得採自深入淺出設計模式一書:

 

獨體模式可說是類別圖最簡單的一個設計模式。事實上類別圖中也只定義了一個類別,

我們常常把物件的類別定義下來,然後呼叫建構式來建立許多物件的實體。

原因是因為類別被看待成一個分類,事實上他就像定義一個種族。用來建立同一種類的物件的集合。

因此我們常常建立鴨子類別,然後建立許多鴨子,又建立Pizza類別,然後生產出許多Pizza。

正是因為生活中,我們必須因應需求(需求決定你分析的角度),去大量製造這些產品或生物(呵)。

然而,回頭看看生活週遭,有哪些物件須不需要大量生產,特別它是在操作的時候,只”需要”一個,而且”只能”有一個?

 

例如說: Thread Pool、Cache、Registry、Login User

上述的例子假如大量的製造就會導致許多問題產生。最明顯的就是行為異常、資源使用過量、或者是不一致的結果!

回歸先前的物件種族論點。反觀我們現行的程式環境,資源其實是非常有限的。

這些寶貴而且稀有的記憶體、共用的區塊、以及程式片段。在存取、操作以及安全上都應該只存在一個實體。

 

最明顯的例子就是程式中的全域變數,全域變數讓所有其他程式能夠存取到變數的值以及程式之間約定好的物件。

在程式碼最常見的作法就是透過宣告靜態變數即可。

 

只不過,透過全域變數還是有一些缺點:物件一開始就必須建立好,但是假如程式過程中並沒有使用到這個物件的話,

就形成了一種資源的浪費!這類的物件被大量地宣告其實是非常耗費資源的。

 

因此!!透過獨體模式,可以讓物件在被需要的時候才建立物件!來看看吧!

我們通常會採用下列的方式來建立物件

   1: public ClassA{ //類別A
   2:     private ClassA(){} //建構式
   3: }

看看一些問題,第一個,我們不能讓所有的人都可以隨意的來建立物件A的實體!有時不當的開放也是一項程式設計缺點。

為了不讓其他人可以隨意地建立與多次的實例化物件,至少,我先要把物件的權力攬在自己的手上!(建構式設為私有)

 

但這好像有一些不合常理,看出來了嗎?

 

含有私有的建構式之類別好像不能實體化耶

必竟類別A的實體,才能呼叫類別A的建構式,但是沒有這樣的實體,因為沒有其他的類別可以實體化這個類別A。

這正是「雞生蛋」、「蛋生雞」的難題不是嗎?

 

而解決這個問題的方法就是宣告一個 「類別方法」,而不是透過物件方法去建立實物。

   1: public Class ClassA{
   2:    private ClassA(){}
   3:    public static createInstance(){
   4:         return new ClassA();
   7:    }
   8: } 
  11: //建立物件
  12: ClassA.createInstance();

是的,建立靜態方法就可以了。這個方法是依附在類別上,而不是物件上,因此不用透過實體化物件來呼叫了。

但是還有一些問題,能夠繼續修改程式,建立出唯一一個的類別A的實體嗎?

 

來看看,以下是一個經典的獨體模式的程式碼:

   1: public class Singleton{
   2:     //透用一個靜態變數記錄實體
   3:     private static Singleton uniqueInstance;
   4:     //建構式的啦
   5:     private Singleton(){}
   6:     public static Singleton getInstance(){
   7:         if (uniqueInstance == null)
   8:             uniqueInstance = new Singleton();
   9:         return uniqueInstance;
  10:     }
  11: }

正常來看,一眼就可以看出這個程式正是為了達成獨體模式所完成的程式嘛。

從getInstance中的判斷式中可以知道,如果不需要此實體,就不會產生此實體

只有在需要時,才會產生。這邊給他一個名詞:Lazy Instantiaze(拖延實體化)

 

這也是獨體模式的一個特色。當外人要取得此類別的實體時,不再是主動的建構實例!

而是請求得到一個實體!呼叫了getInstance()這個方法,物件就會立刻現身,隨時可以工作。

事實上可能是當下被建立,也有可能是早前被建立出來的。

 

再來看看這次又有什麼需求來挑戰獨體模式呢?

以下實例是自己想的

假設:大家都知道「古坑樂園」現在都有很多另人心臟狂跳的遊樂設施,而且現在這些器材都是透過現代化電腦來控制。

包含設施的啟動、安全設備的啟動、檢查與設施的執行。

看看它們的程式,將發現這些程式必須寫的很小心(當然本篇只是簡單化),要努力防止不好的事情發生。例如:設施啟動的時候,安全設備卻還沒啟用!或是還在運行的時候,安全設備就被解除!(恐怖極了)

   1: public class Roller_Coaster
   2: {
   3:     private bool safeGuarded; //啟用安全裝置!
   4:     private bool facilityState; //設備運行狀態
   5:  
   6:     public Roller_Coaster()
   7:     {
   8:         safeGuard = false; //安全裝置解除,可以讓乘客上座與離座
   9:         facilityRun = false; //運行狀態為停止(False)
  10:     }
  11:  
  12:     public void run()
  13:     {
  14:         //執行的時候,必須檢查安全裝置是不是啟用
  15:         //若沒有啟用,就必須啟用,才能執行設施
  16:        if (!isGuarded()){
  17:            safeGuarded = true;
  18:            facilityState = true;
  19:        }
  20:     }
  21:  
  22:     public void stop()
  23:     {
  24:         //在停止的時候,必須確認設施正在執行,而且安全設備是啟用的
  25:         if (isRuning() && isGuarded())
  26:         {
  27:             facilityState = false;
  28:             safeGuarded = false;
  29:         }
  30:     }
  31:  
  32:     public Boolean isRuning()
  33:     {
  34:         return facilityState;
  35:     }
  36:  
  37:     public bool isGuarded()
  38:     {
  39:         return safeGuarded;
  40:     }
  41: }
  42:  

然而,假如上述的雲霄飛車萬一同時有兩個實體,可不可能會發生不好的事情呢?

假如現在列車要運行了,結果改”錯”了列車的實體,讓安全裝置解除,或是未被啟用。這個風險是有可能發生的…

 

因此我們身負重任,來改進古坑樂園的雲宵飛車類別吧,把這個類別改成獨體模式!!

   1: //唯一的物件
   2: private static Roller_Coaster uniqueInstance;
   3:  
   4:  
   5: //改成私有的
   6: private Roller_Coaster()
   7: {
   8:     safeGuard = false; //安全裝置解除,可以讓乘客上座與離座
   9:     facilityRun = false; //運行狀態為停止(False)
  10: }
  11:  
  12: public static Roller_Coaster getInstance(){
  13:     if (uniqueInstance == null)
  14:         uniqueInstance = new Roller_Coaster();
  15:     return uniqueInstance;
  16: }

沒錯,上述程式列出關鍵的部分。

 

定義獨體模式

經典的獨體模式就如上述程式所顯示。回頭來看看~定義:

定義:獨體模式確保一個類別只有一個實體,並給它一個存取的全域點(Global Point)。

來自於深入淺出設計模式一書(Headfirst in Design Pattern)

 

這樣的實踐方式,也提供了一個方法,讓程式在任何地方都可以取得此獨體,當需要的時候

請類別提供,就可以得到獨體物件,這種作法透過Lazy Instantiaze的方式建立獨體,這種作法對資源集中物件特別的重要。

透過獨體模式,提供和全域變數一樣簡單的存取方式,但是又比全域變數多了一個優點(Lazy Instantiaze)呢。

 

類別圖如下圖所示:

image

 

然而,潛在的問題有可能發生吧,會發生災難的風險還是非常顯著的……

看來雲宵飛車的系統要讓我們失望了…是哪裡呢?儘管利用了經典的獨體實踐改善了程式碼。

但是Roller_Coaster中的Run的方法,竟然允許在啟動的過程中,讓人解除安全裝置,或是一直運行下去,這可是會出人命的啊…

狀況是來自於你有可能產生兩個設施的物件,導致問題的發生。

 

深入淺出設計模式一書提到,新的獨體模式雖然順利,一開始只是為了讓程式更有執行效率。一旦加入”多執行緒”的功能後,就有可能出事…這是潛在的問題

為什麼呢?以JVM的高度來看,假如程式是以以下的Timing執行,這個程式就有可能產生出兩個物件,導置擾亂程式。

image

 

你可以看到程式執行時互相重疊的部分。

然而,能改善多執行緒所產生的潛在問題嗎?(必竟我們必須認定所有的程式都是多執行緒的程式)

1.在Java中可以採用同步化(synchronized)的方法(指同一個方法迫使執行緒僅有一個能被執行)

雖然這個方法可以解決問題,卻會降低效率呢,必竟每次只有第一次執行此方法時,才真正需要同步化。

然後其他每次呼叫這個方法卻仍執行同步化。反正會是一種累贅。

 

2.使用「率先」建立實體,而不用拖延實體化(Lazy instaniaze)的作法。也就是存取實體前,一定會先建立此實體

這個方式可以改善第一種方法造成的效率下降,而且保證安全。

 

3雙重上鎖技巧.(Double-Checked Locking)

以下以古坑樂園的雲宵飛車來介紹第三項雙重上鎖技巧

   1: //唯一的物件
   2:      private volatile static Roller_Coaster uniqueInstance;
   3:      public static Roller_Coaster getInstance()
   4:      {
   5:          if (uniqueInstance == null)
   6:          {
   7:              lock (Roller_Coaster.uniqueInstance)
   8:              {
   9:                  if (uniqueInstance == null)
  10:                  {
  11:                      uniqueInstance = new Roller_Coaster();
  12:                  }
  13:              }
  14:          }
  15:          return uniqueInstance;
  16:      }

你可以看到,在getInstance的存取點方法的地方,我們建立了兩次的檢查實體的動作,而第二個檢查實體必須Lock住uniqueInstance,確保他在這段時間內不會被修改!

這個是.NET中的作法,移到Java中就是透過Synchorized的關鍵字!

而另一個改變是第二行的volatile關鍵字,這個關鍵字確保,當uniqueInstance變數被實體化的時候,多個執行緒處理uniqueInstance的作法是正確的。

深入淺出設計模式建議,若效能是你關心的重點,那這個作法可以幫你大大減少getInstance的時間耗費。

以上是獨體模式的分享,因為內涵與目的非常的明確,僅簡單作為筆記參考。

我們可能還是搞不清楚全域變數與獨體模式。

我們再度聚焦此模式的目的:確保類別僅有一個實例,並且提供全域存取的方法。然而全域變數可以提供全域的存取,但是不能確保只有一個實體。

後記:在設計程式碼的時候,想想這些模式能帶來什麼樣的好處,甚至是是否適合被繼承?獨體模式並不一定適合設計進入程式庫中

適合使用獨體模式的機會很多。只要當你需要控制實體個數時,就應當使用獨體模式

另外,回文中也有很多獨體模式的混合應用,可以參考之^^。