程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> .NET實例教程 >> 探究Singleton設計模式

探究Singleton設計模式

編輯:.NET實例教程
在開發軟件應用程序過程中,隨著應用程序的開發,會出現重復性的模式。隨著整個軟件系統的開發,很多相同的模式會逐漸顯現出來。
  這種重復性模式概念在其他應用中是非常明顯的。汽車制造就是一種此類應用。很多不同的汽車型號使用相同的子構件,包括大多數基本部件(例如,燈泡和緊固零件)以及較大的構件(例如,底盤和發動機)。

  在住宅建築中,重復性模式概念適用於螺絲和螺釘以及整體總體建築物配電系統。無論組建的小組是為了開發新的汽車設計還是新的建築物設計,它通常不必沒有考慮到以前已解決的問題。如果設計和建築住宅的小組必須重新構思和設計房子的每一個組成部分,則整個過程所花的時間比現在要長得多。門高或燈開關功能等許多設計決策(例如,門高或燈開關功能)很容易理解。房為滿足給房子不同部分提供洗手功能的要求,房屋設計師不必重新設計和重新建造不同類型的輸供水和蓄水設施,以便達到為房子不同部分提供洗手功能的要求:標准水槽以及標准的熱水和冷水輸入接頭和排水輸出接頭是很容易理解非常常見的房屋建築構件。可以將重復性模式概念反復應用於我們周圍的幾乎每樣東西上,包括軟件。

  汽車和住宅建築示例有助於在軟件設計和構造中體現某些一般性的抽象概念。易於理解且明確定義的通用功能部件的概念是設計模式的源動力,它也是其他兩篇設計模式文章探究工廠設計模式和探究觀察者設計模式的重點。這些模式幾乎涵蓋了面向對象的軟件設計的各個方面,包括對象創建、對象交互和對象生存期。在本文中,我們將討論Singleton模式,它包含在創造性模式系列中。

  創造性模式指示如何以及何時創建對象。很多實例需要只能通過創造性方法解決的特殊行為,而不是在創建實例後強制實施所需的行為。此類行為要求最好的例子之一包含在Singleton模式中。Singleton模式在《設計模式:可復用的面向對象軟件的基礎》這一經典參考書目中有正式的定義,該書的作者包括Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides(也稱為四人組或GoF)。在設計模式中,此模式是最簡單也是使用最廣泛的模式之一。但是,正如我們將會看到的一樣,在實現此模式時可能會出現一些問題。本文試圖通過Singleton模式的多個早期實現來從頭開始分析Singleton模式,以及如何在Microsoft .Net應用程序開發中發揮其最佳用途。

  Singleton模式 

  按照設計模式中的定義,Singleton模式的用途是“ensure a class has only one instance, and provide a global point of Access to it(確保每個類只有一個實例,並提供它的全局訪問點)”。

  它可以解決什麼問題,或者換句話說,我們使用它的動機是什麼?幾乎在每個應用程序中,都需要有一個從中進行全局訪問和維護某種類型數據的區域。在面向對象的(OO)系統中也有這種情況,在此類系統中,在任何給定時間只應運行一個類或某個類的一組預定義數量的實例。例如,當使用某個類來維護增量計數器時,此簡單的計數器類需要跟蹤在多個應用程序領域中使用的整數值。此類需要能夠增加該計數器並返回當前的值。對於這種情況,所需的類行為應該僅使用一個類實例來維護該整數,而不是使用其它類實例來維護該整數。

   最初,人們可能會試圖將計數器類實例只作為靜態全局變量來創建。這是一種通用的方法,但實際上只解決一部分問題;它解決了全局可訪問性問題,但沒有采取任何措施來確保在任何給定的時間只運行一個類實例。應該由類本身來負責只使用一個類實例,而不是由類用戶來負責。應該始終不要讓類用戶來監視和控制運行的類實例的數量。

  所需要的是使用某種方法來控制如何創建類實例,然後確保在任何給定的時間只創建一個類實例。這會確切地給我們提供所需的行為,並使客戶端不必了解任何類細節。

  邏輯模型

  Singleton模型非常簡單直觀。(通常)只有一個Singleton實例。客戶端通過一個已知的訪問點來訪問Singleton實例。在這種情況下,客戶端是一個需要訪問唯一Singleton實例的對象。圖1以圖形方式顯示此關系。


  物理模型

  Singleton模式的物理模型也是非常簡單的。但是,隨著時間的推移,實現Singleton的方式也略有不同。讓我們看一下原始的GoFSingleton實現。圖2顯示按設計模式所定義的原始Singleton模式的UML模型。


  我們看到的是一個簡單的類圖表,顯示有一個Singleton對象的私有靜態屬性以及返回此相同屬性的公共方法Instance()。這實際上是Singleton的核心。還有其他一些屬性和方法,用於說明在該類上允許執行的其他操作。為了便於此次討論,讓我們將重點放在實例屬性和方法上。

  客戶端僅通過實例方法來訪問任何Singleton實例。此處沒有定義創建實例的方式。我們還希望能夠控制如何以及何時創建實例。在OO開發中,通常可以在類的構造函數中最好地處理特殊對象的創建行為。這種情況也不例外。我們可以做的是,定義我們何時以及如何構造類實例,然後禁止任何客戶端直接調用該構造函數。這是在Singleton構造中始終使用的方法。讓我們看一下設計模式中的原始示例。通常,將下面所示的C++Singleton示例實現代碼示例視為Singleton的默認實現。本示例已移植到很多其他編程語言中,通常它在任何地方的形式與此幾乎相同。

  C++ Singleton示例實現代碼

//Declaration
class Singleton{
public: 
static Singleton* Instance();
protected: 
Singleton();
private:
static Singleton* _instance;
}

// Implementation 
Singleton* Singleton::_instance = 0;

Singleton* Singleton::Instance() {
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
}
  讓我們先花點時間分析一下此代碼。該簡單類有一個成員變量,此變量是指向該類自身的指針。注意,構造函數是受保護的,並且只有公共方法才是實例方法。在實例方法實現中,有一個控制塊(if),它檢查成員變量是否已初始化,如果沒有的話,則創建一個新實例。控制塊中這種惰性初始化意味著僅在第一次調用Instance()方法時初始化或創建Singleton實例。對於很多應用程序,這種方法效果很好。但對於多線程應用程序,這種方法證明具有潛在危險的副作用。如果兩個線程同時進入控制塊,則可能會創建該成員變量的兩個實例。要解決這一問題,您可能想只將重要部分放在控制塊周圍以確保線程安全。如果您這樣做,則將對實例方法的所有調用進行序列化處理,並且可能會對性能產生不利影響(取決於應用程序)。正是由於這個原因,創建了此模式的另一個版本,它使用某種稱為雙重檢驗機制的功能。下一個代碼示例顯示使用Java語法的雙重檢驗鎖定。

  使用Java語法的雙重檢驗鎖定Singleton代碼

//C++ port to Java
class Singleton
{
public staticSingletonInstance() {
if (_instance == null) {
synchronized (Class.forName("Singleton")) {
if (_instance == null) {
_instance = new Singleton();
}
}
}
return _instance; 
}
protected Singleton() {}
private staticSingleton_instance = null;
}
  在使用Java語法的雙重檢驗鎖定Singleton代碼示例中,我們直接將C++代碼移植到Java代碼,以便利用Java關鍵部分塊(已同步)。主要差別是不再有單獨的聲明和實現部分,沒有指針數據類型,並且采用了新的雙重檢驗機制。雙重檢驗發生在第一個IF塊上。如果成員變量為空,則執行進入關鍵部分塊,該塊再次雙重檢驗該成員變量。僅在通過此最終測試後,才會實例化該成員變量。一般來說,兩個線程無法使用這種方法創建兩個類實例。另外,因為在第一次檢查時沒有出現線程阻塞,所以對此方法的大多數調用不會由於必須進入鎖定而導致性能下降。目前,在實現Singleton模式時,很多Java應用程序中都廣泛使用這種方法。

  這種方法很巧妙,但也有瑕疵。某些優化編譯器可以將惰性初始化代碼優化掉或對其重新進行排序,並且會重新產生線程安全問題。

  邏輯模型

  Singleton模型非常簡單直觀。(通常)只有一個Singleton實例。客戶端通過一個已知的訪問點來訪問Singleton實例。在這種情況下,客戶端是一個需要訪問唯一Singleton實例的對象。圖1以圖形方式顯示此關系。


  物理模型

  Singleton模式的物理模型也是非常簡單的。但是,隨著時間的推移,實現Singleton的方式也略有不同。讓我們看一下原始的GoFSingleton實現。圖2顯示按設計模式所定義的原始Singleton模式的UML模型。


  我們看到的是一個簡單的類圖表,顯示有一個Singleton對象的私有靜態屬性以及返回此相同屬性的公共方法Instance()。這實際上是Singleton的核心。還有其他一些屬性和方法,用於說明在該類上允許執行的其他操作。為了便於此次討論,讓我們將重點放在實例屬性和方法上。

  客戶端僅通過實例方法來訪問任何Singleton實例。此處沒有定義創建實例的方式。我們還希望能夠控制如何以及何時創建實例。在OO開發中,通常可以在類的構造函數中最好地處理特殊對象的創建行為。這種情況也不例外。我們可以做的是,定義我們何時以及如何構造類實例,然後禁止任何客戶端直接調用該構造函數。這是在Singleton構造中始終使用的方法。讓我們看一下設計模式中的原始示例。通常,將下面所示的C++Singleton示例實現代碼示例視為Singleton的默認實現。本示例已移植到很多其他編程語言中,通常它在任何地方的形式與此幾乎相同。

  C++ Singleton示例實現代碼

//Declaration
class Singleton{
public: 
static Singleton* Instance();
protected: 
Singleton();
private:
static Singleton* _instance;
}

// Implementation 
Singleton* Singleton::_instance = 0;

Singleton* Singleton::Instance() {
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
}
  讓我們先花點時間分析一下此代碼。該簡單類有一個成員變量,此變量是指向該類自身的指針。注意,構造函數是受保護的,並且只有公共方法才是實例方法。在實例方法實現中,有一個控制塊(if),它檢查成員變量是否已初始化,如果沒有的話,則創建一個新實例。控制塊中這種惰性初始化意味著僅在第一次調用Instance()方法時初始化或創建Singleton實例。對於很多應用程序,這種方法效果很好。但對於多線程應用程序,這種方法證明具有潛在危險的副作用。如果兩個線程同時進入控制塊,則可能會創建該成員變量的兩個實例。要解決這一問題,您可能想只將重要部分放在控制塊周圍以確保線程安全。如果您這樣做,則將對實例方法的所有調用進行序列化處理,並且可能會對性能產生不利影響(取決於應用程序)。正是由於這個原因,創建了此模式的另一個版本,它使用某種稱為雙重檢驗機制的功能。下一個代碼示例顯示使用Java語法的雙重檢驗鎖定。

  使用Java語法的雙重檢驗鎖定Singleton代碼

//C++ port to Java
class Singleton
{
public staticSingletonInstance() {
if (_instance == null) {
synchronized (Class.forName("Singleton")) {
if (_instance == null) {
_instance = new Singleton();
}
}
}
return _instance; 
}
protected Singleton() {}
private staticSingleton_instance = null;
}
  在使用Java語法的雙重檢驗鎖定Singleton代碼示例中,我們直接將C++代碼移植到Java代碼,以便利用Java關鍵部分塊(已同步)。主要差別是不再有單獨的聲明和實現部分,沒有指針數據類型,並且采用了新的雙重檢驗機制。雙重檢驗發生在第一個IF塊上。如果成員變量為空,則執行進入關鍵部分塊,該塊再次雙重檢驗該成員變量。僅在通過此最終測試後,才會實例化該成員變量。一般來說,兩個線程無法使用這種方法創建兩個類實例。另外,因為在第一次檢查時沒有出現線程阻塞,所以對此方法的大多數調用不會由於必須進入鎖定而導致性能下降。目前,在實現Singleton模式時,很多Java應用程序中都廣泛使用這種方法。

  這種方法很巧妙,但也有瑕疵。某些優化編譯器可以將惰性初始化代碼優化掉或對其重新進行排序,並且會重新產生線程安全問題。

  另一種試圖解決此問題的方法可能是,在成員變量聲明中使用volatile關鍵字。這應該告訴編譯器不要對代碼重新排序,並且放棄優化。目前,這是唯一建議的JVM內存模型,並且不會立即解決該問題。

  實現Singleton的最好方法是什麼?最終(而不是碰巧),Microsoft .net框架解決了所有這些問題,從而更易於實現Singleton,卻不會產生我們目前討論的不利副作用。.Net框架以及C#語言允許我們在必要時通過替換語言關鍵字,將上述的Java語法移植到C#語法。因此,Singleton代碼變為以下內容:

  以C#編碼的雙重檢驗鎖定

// Port to C#
class Singleton
{
public staticSingletonInstance() {
if (_instance == null) {
lock (typeof(Singleton)) {
if (_instance == null) {
_instance = new Singleton();
}
}
}
return _instance; 
}
protected Singleton() {}
private static volatileSingleton_instance = null;
}
  此處,我們替換了鎖定關鍵字來執行關鍵部分塊,使用typeof操作並添加volatile關鍵字,以確保沒有對代碼進行優化程序重新排序。雖然此代碼或多或少是GoFSingleton模式的直接移植,但它可達到我們的目的,並且我們可獲得所需的行為。此代碼還說明了將C++移植到Java和將Java移植到C#代碼的一些相似之處和主要差別。但是,正如任何代碼移植一樣,通常目標語言或平台的一些優點可能在移植過程中失去。需要做的就是對代碼重構,以便利用新目標語言或平台的功能。

  在前面的每個代碼示例中,Singleton的原始實現隨時間的推移而發生變化,以解決在每個新模式實現中發現的問題。一些問題(例如,線程安全)要求對大多數實現進行更改,以滿足在目前應用程序中日益增長的需要並解決演變發展問題。.Net在應用程序開發中提供了一個演變步驟。可以在“框架”級別解決前面示例中出現的很多亟待解決的問題,而不是在實現級別解決。雖然上一個示例顯示了一個使用.NET框架和C#的有效Singleton類,但只需更好地利用.NET框架本身就可以大大簡化此代碼。以下示例使用.Net,它是一個松散地基於原始GoF模式的最小限度的Singleton類,並且仍然可獲得類似的行為。

  .NetSingleton示例

//.Net Singleton
sealed class Singleton
{
private Singleton() {}
public static readonlySingletonInstance = new Singleton();
}
  此版本已大大簡化並且更加直觀。它仍然是Singleton嗎?讓我們看一下更改了哪些內容,然後再做決定。我們修改了要密封的類本身(該類密封後是不可繼承的),刪除了惰性初始化代碼,刪除了Instance()方法,並且對_instance變量做了大量的修改。對_instance變量所做的更改包括修改對公共方法的訪問級別,將變量標記為只讀,以及在聲明時初始化該變量。此處,我們可以直接定義所需的行為,而不關心實現的潛在有害的副作用。那麼,使用惰性初始化有什麼優點以及使用多個線程有什麼危險呢?在.Net框架中內置了所有正確的行為。讓我們先看第一種情況:惰性初始化。

  最初使用惰性初始化的主要原因是要獲取僅在第一次調用Instance()方法中創建實例的行為,還因為C++規范中具有某種開放性,並不定義靜態變量的確切初始化順序。要在C++中獲得所需的Singleton行為,必須采用涉及使用惰性初始化的運算方法。我們真正關心的是在第一次(在該情況下)調用實例屬性中創建該實例,還是在此調用之前創建該實例的,並且類中的靜態變量是否有已定義的初始化順序。對於.Net框架,這就是我們獲取的行為。在JIT過程中,當(且僅當)任何方法使用靜態屬性時,“框架”將初始化此靜態屬性。如果沒有使用該屬性,則不會創建實例。更准確地說,在JIT過程中發生的事情就是,在任何調用方使用該類的任何靜態成員時構造和加載該類。在這種情況下,結果是相同的。

  那麼,線程安全初始化呢?“框架”也解決了這一問題。“框架”內部保證靜態類型初始化的線程安全。換句話說,在上面的示例中,只創建一個Singleton類實例。還要注意,用於保存類實例的屬性字段稱為實例。此選項更好地說明了,在本文中的討論過程中,此值是類的實例。在“框架”本身中,雖然使用的屬性名稱稱為值,但有多個類使用此類型的Singleton。概念完全相同。

  對類所做的其他更改意味著禁止劃分子類。添加密封類修飾符可確保不會將該類劃分為子類。GoFSingleton模式詳細介紹了試圖對Singleton劃分子類所產生的問題,該劃分通常並不是小事。在大多數情況下,可以很容易地開發沒有父類的Singleton,並且添加劃分子類功能會增加通常根本不需要的新的復雜性級別。隨著復雜性的提高,測試、培訓和文檔編制等所需的時間也會增加。通常,除非絕對必要,否則您不希望提高任何代碼的復雜性。

  讓我們看一下如何使用Singleton。使用我們最初的計數器的有關動機的概念,我們可以創建一個簡單的Singleton計數器類並說明我們將如何使用它。圖3顯示了UML類說明將包含什麼內容。


  相應的類實現代碼以及示例客戶端使用如下所示。

  示例Singleton使用

sealed class SingletonCounter {
public static readonly SingletonCounter Instance = 
new SingletonCounter();
private long Count = 0;
private SingletonCounter() {}
public long NextValue() {
return ++Count;
}
}

class SingletonClIEnt {
[STAThread]
static void Main() {
for (int i=0; i<20; i++) {
Console.WriteLine("NextSingletonvalue: {0}", 
SingletonCounter.Instance.NextValue());
}
}
}
  此處,我們還創建了一個Singleton類來維護具有long類型的增量計數。客戶端是一個簡單的控制台應用程序,它顯示計數器類的20個值。雖然此示例極其簡單,但它卻說明了如何使用.Net來實現Singleton,然後將其用在應用程序中。

  小結

  Singleton設計模式是一個非常有用的機制,可用於在面向對象的應用程序中提供單個對象訪問點。無論使用的是什麼實現,該模式提供一個大家所熟知的概念,以便其在設計和開發小組之間方便地進行共享。但是,正如我們所發現的一樣,注意到這些實現有多大差異及其潛在的副作用也是非常重要的。.Net框架為模式實現者在設計所需的功能類型方面提供了很大的幫助,實現者無需處理本文中所討論的很多副作用。在正確實現後,可以證實模式的最初目的的有效性。

  設計模式是非常有用的軟件設計概念,可使小組將重點放在提供最佳類型的應用程序上,而不考慮它們是什麼應用程序。關鍵在於正確而有效地使用設計模式,目前有很多關於將設計模式用於Microsoft .Net方面的MSDN系列文檔,其中介紹了如何正確而有效地使用設計模式。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved