1. 程式人生 > >C#設計模式之單例模式

C#設計模式之單例模式

早就想寫幾篇設計模式相關的部落格了,馬上要開始找工作了,藉此機會複習一下設計模式同時也學習一下Markdown。本科階段寫過一個小遊戲,當時能力有限,程式碼能力以及對設計模式的理解和運用都不夠,後來研究生期間,由於《軟體結構設計與模式分析》這門課的大作業需要我們編寫並分析一個軟體,軟體型別不限,由於覺得這款小遊戲題材不錯,又有趣味性,所以借鑑了該遊戲的思路並對它進行了重構,不僅介面進行了大量優化,同時也加入了一些設計模式,大大提高了軟體的擴充套件性,這裡結合這個小遊戲,分析幾個遊戲中使用到的設計模式: 單例模式,策略模式和工廠方法模式。

部落格中沒有對整個遊戲的設計做詳細的介紹,只是借這個遊戲分析一下設計模式,遊戲使用C#實現,遊戲的VS工程和相關資料我上傳到CSDN上了

點選下載。大家可以結合程式碼看本文,這樣能夠更好的理解這個遊戲和遊戲中的幾個設計模式。

遊戲簡介

簡介

遊戲是一個非常簡單的RPG小遊戲,遊戲中主要有兩類角色,分別為Hero-英雄和Enemy-敵人(怪獸),英雄是由玩家控制的角色,怪獸是系統控制的角色,其中怪獸分為不同等級,有小怪和大怪,遊戲內容比較簡單,就是雙方發射子彈攻擊對方,如果怪獸將英雄的生命值打為0,遊戲結束,如果英雄將最後的大怪生命值打為0,遊戲勝利。

介面演示

本來想做成GIF動畫演示的,但是由於GIF檔案比較大,上傳不了,這裡貼張圖片,展示一下游戲的介面,大家可以下載原始碼執行,就可以看到整個遊戲執行過程了。
操作說明:”X”鍵發射子彈,方向鍵控制人物的移動

遊戲介面

遊戲整體結構

開啟VS工程,開啟其中的類圖檔案ClassDiagram1.cd,就可以看到整個遊戲的類圖了
VS工程

遊戲的類圖如下
這裡寫圖片描述

簡單分析一下游戲的結構

  • Element:所有角色的根類
  • RoAndMi:繼承自Element,是角色和子彈的基類
  • Roles及其子類:遊戲中的所有角色
  • Missiles及其子類:遊戲中所有角色的子彈
  • FireBehavior及其子類:遊戲中所有角色的發射子彈的行為
  • HitCheck:遊戲的主控類,用來控制遊戲中所有元素
    遊戲詳細的實現過程,讀者可以看原始碼,結合類圖看原始碼,相信讀者很快就能非常清楚整個遊戲了

下面的三個部分是遊戲的核心

  • Roles及其子類:遊戲中的所有角色
  • FireBehavior及其子類:遊戲中所有角色的發射子彈的行為
  • HitCheck:遊戲的主控類,用來控制遊戲中所有元素

分析遊戲的時候,要把握好這三塊。
下面我們就結合這個小遊戲,分析三種設計模式:單例模式,策略模式和工廠方法模式。

單例模式

定義

確保一個類只有一個例項,並提供一個全域性訪問點。[1]P177(表示在參考文獻[1]的177頁,下同)

經典的單例模式實現

public class Singleton {
    private static Singleton uniqueInstance;

    // other useful instance variables here

    private Singleton() {}

    public static Singleton GetInstance() 
    {
        if (uniqueInstance == null) 
        {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

    // other useful methods here
}

總結一下單例的實現就是:一個私有,兩個靜態

  • 一個私有
    就是私有建構函式

    單例模式的思想就是一個類只有一個例項,即外部任何類都不能例項化該類,那麼什麼樣的類外部不能例項化呢?我們知道,例項化一個類的時候,需要呼叫建構函式,而一般建構函式都是public的,所以能夠被外部呼叫,所以能夠在外部例項化,當將建構函式設定為private時,外部就不能呼叫類的構造函數了,也就不能例項化該類了,該類只能在類的內部例項化。這個思想是實現單例模式的關鍵。

  • 兩個靜態

    1.靜態成員變數uniqueInstance,該成員變數就是類的唯一例項
    2.靜態方法GetInstance(),用來獲取該類的唯一例項

    前面提到了使用私有建構函式是實現單例模式的關鍵,那麼下面的問題就是怎麼在外部獲取該單例呢?由於任何外部類都不能例項化該類,所以我們無法通過使用類的物件來呼叫類裡面的方法獲取單例(即不能通過Singleton singleton=new Singleton();singleton.GetInstance()來獲取單例),只能通過類裡面的靜態方法,通過類名呼叫靜態方法(Singleton.GetInstance())來獲取單例,而靜態方法只能呼叫靜態成員,所以類的成員變數也必須是靜態的。

適用性

當一個類只能有一個例項而且客戶可以從一個眾所周知的訪問點訪問它時。[2]P84

對有些類來說,只有一個例項很重要,如執行緒池,登錄檔,檔案系統等
雖然全域性變數也可以提供全域性訪問點,但是不能防止你例項化多個物件

遊戲中的實現

類圖檔案中雙擊HitCheck類,就能看到程式碼,當然也可以在工程中直接開啟HitCheck.cs

    /// <summary>
    /// 主控類,負責遊戲中的各種角色的管理
    /// 1.AddElement()---新增元素
    /// 2.RemoveElement()----刪除元素
    /// 3.Draw()----元素的繪製
    /// 4.DoHitCheck()---元素之間的碰撞檢測
    /// 5.Restart()---重新開始遊戲
    /// </summary>
    public class HitCheck
    {
        //遊戲中的角色
        private Hero myHero = null;
        private List<MissileHero> missileHero = new List<MissileHero>();
        private List<Roles> enemy = new List<Roles>();
        private List<Missiles> enemyMissile = new List<Missiles>();
        private List<Element> bombs = new List<Element>();
        /// <summary>
        /// 建構函式私有化,禁止在其他地方例項化
        /// </summary>
        private HitCheck() { }

        private static  HitCheck instance;

        public static HitCheck GetInstance()
        {
            if (instance == null)
            {
                instance = new HitCheck();
            }
            return instance;
        }
        ...
   }

這個程式碼看上去是不是很熟悉,這就是個典型的單例模式的實現:一個私有,兩個靜態.

為什麼要使用單例模式

剛開始寫遊戲的時候是沒有用的,慢慢發現,遊戲中的角色一旦過多,角色就很難管理,如角色的產生,角色的死亡,包括角色之間的碰撞檢測。一旦遊戲中要增加角色需要修改的程式碼很多,維護量比較大,所以就想設計一個類,實現對遊戲中所有角色的管理,這樣就可以很方便的對遊戲中的角色進行管理。這個類主要控制遊戲中的所有角色,包括對所有元素的增加,刪除,以及碰撞檢測(如英雄是否被敵人的子彈打中),這就要求該類只能有一個例項,不能有多個例項。不然遊戲就會出錯,所以設計為單例。讀者分析一下HitCheck的原始碼就非常清楚其中使用單例的原因了。

多執行緒問題

經過上面的介紹和分析,讀者對基本單例模式的實現和原理應該比較清楚了,那麼是否這樣的單例模式就非常好了呢?下面我們討論一下在多執行緒中的問題。
還是看上面經典單例模式的程式碼

public class Singleton {
    private static Singleton uniqueInstance;

    // other useful instance variables here

    private Singleton() {}

    public static Singleton GetInstance() 
    {
        if (uniqueInstance == null) 
        {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

    // other useful methods here
}

假設現在有兩個執行緒,以下是他們的執行步驟:
多執行緒
多執行緒中,由於每個執行緒執行的順序不確定,就有可能產生2個例項。
那怎麼解決呢?這裡提供以下兩種方式,有更好的方式,歡迎大家提出來。

方法1:”急切”例項化

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton GetInstance() {
        return uniqueInstance;
    }
}

程式碼中,當類被載入時,靜態變數uniqueInstance 會被初始化,此時類的私有建構函式會被呼叫,單例類的唯一例項將被建立。多執行緒的時候,由於類載入的時候就建立了例項,所以不會出現多個例項的情況。

方法2:“雙重檢查加鎖”

class Singleton 
{ 
    private static volatile Singleton instance = null; 
    //程式執行時建立一個靜態只讀的輔助物件
    private static readonly object syncObject= new object();

    private Singleton() { } 

    public static Singleton GetInstance() 
    { 
        //第一重判斷,先判斷例項是否存在,不存在再加鎖處理
        if (instance == null) 
        {
            //臨界區!
            //加鎖的程式在某一時刻只允許一個執行緒訪問
            lock(syncObject)
            {
                //第二重判斷
                if(instance==null)
                {
                    instance = new Singleton();  //建立單例例項
                }
            }
        }
        return instance; 
    }
}

為了更好地對單例物件的建立進行控制,此處使用了一種被稱之為雙重檢查加鎖機制。在雙重檢查鎖定中,當例項不存在且同時有兩個執行緒呼叫GetInstance()方法時,它們都可以通過第一重“instance==null”判斷,然後由於lock鎖定機制,只有一個執行緒進入lock中執行建立程式碼,另一個執行緒處於排隊等待狀態,必須等待第一個執行緒執行完畢後才可以進入lock鎖定的程式碼,如果此時不進行第二重“instance==null”判斷,第二個執行緒並不知道例項已經建立,將繼續建立新的例項,還是會產生多個單例物件,因此需要進行雙重檢查。

volatile關鍵字
volatile修飾的成員變數在每次被執行緒訪問時,都強迫從共享記憶體中重讀該成員變數的值。當成員變數發生變化時,強迫執行緒將變化值回寫到共享記憶體(執行緒共享程序的記憶體)。這樣,讀取這個變數的值時候每次都是從momery裡面讀取而不是從cache讀,這樣做是為了保證讀取該變數的資訊都是最新的,而無論其他執行緒如何更新這個變數。
此外,由於使用volatile關鍵字遮蔽掉了一些必要的程式碼優化,所以在效率上比較低,因此需要慎重使用。
如果沒有volatile關鍵字,第二個執行緒就可能沒有及時讀到最新的值,比如程序2在第二重判斷的時候,程序1已經產生了一個例項,但是程序2沒有讀到最新的值,讀到的instance還是為null,那麼就會產生多個例項了,那麼即使使用了雙重檢查加鎖,也有可能產生多個例項。

這兩種方式在[1]P180~P182的處理多執行緒問題中也有非常清楚的闡述,用java描述,深入淺出,講解地非常好。

兩種方式的比較

”急切”例項化在類被載入時就將自己例項化,它的優點在於無須考慮多個執行緒同時訪問的問題,可以確保例項的唯一性;從呼叫速度和反應時間角度來講,由於單例物件一開始就得以建立,因此要優於“雙重檢查加鎖”。但是無論系統在執行時是否需要使用該單例物件,由於在類載入時該物件就需要建立,因此從資源利用效率角度來講,”急切”例項化單例不及“雙重檢查加鎖”單例,而且在系統載入時由於需要建立”急切”例項化單例物件,載入時間可能會比較長。

“雙重檢查加鎖”單例類在第一次使用時建立,無須一直佔用系統資源,實現了延遲載入,但是必須處理好多個執行緒同時訪問的問題。

結束語

遊戲中還有兩個模式:策略模式和工廠方法模式在下面的部落格中講述,讀者可以先看看下載下來的資料中的PPT的相關內容,結合類圖,可以先自行分析遊戲原始碼中的這兩個模式。第一次使用Markdown寫部落格,雖然不太熟練,但是覺得Markdown還是很強大的,寫出的部落格更美觀,更專業。

這裡順便推薦大家一本書:《Head First設計模式》,該本書獲2005年第15屆Jolt大獎,Jolt大獎是軟體行業的”奧斯卡”獎。本書中的每個設計模式都結合具體例項,深入淺出,個人覺得比GOF設計模式更加通俗易懂。
      HeadFirst設計模式
該書的所有程式碼我上傳到CSDN上了,結合書中的程式碼看這本書會更好。
點選下載

參考文獻

[1] 《Head First設計模式(中文版)》 ,中國電力出版社
[2] 《設計模式:可複用面向物件軟體的基礎》(著名的GOF設計模式),機械工業出版社