Unity 遊戲框架搭建 2018 (二) 單例的模板與最佳實踐

Unity 遊戲框架搭建 2018 (二) 單例的模板與最佳實踐

背景

不少開發者或者有經驗的老手都會建議儘可能不要用單例模式,這是有緣由的。git

單例模式是設計模式中最簡單的也是你們一般最早接觸的一種設計模式。在框架的設計中一些管理類或者系統類多多少少都會用到單例模式,好比 QFramework 中的 UIMgr,ResMgr 都是單例。固然在平時的遊戲開發過程當中也會用到單例模式,好比數據管理類,角色管理類等等,以上這些都是很是常見的使用單例的應用場景。github

那麼今天筆者想好好聊聊單例的使用上要注意的問題,但願你們對單例有更立體的認識,並介紹 QFramework 中單例套件的使用和實現細節。設計模式

本篇文章分爲四個主要內容:緩存

  1. 單例的簡介
  2. 幾種單例的模板實現。
  3. 單例的利弊分析。
  4. 單例的最佳實踐:如何設計一個使人愉快的 API?

單例模式簡介

可能說有的朋友不太瞭解單例,筆者先對單例作一個簡單的介紹。微信

定義

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。架構

定義比較簡潔並且不難理解。框架

再引用一個比較有意思的例子函數

俺有6個漂亮的老婆,她們的老公都是我,我就是咱們家裏的老公 Singleton,她們只要說道「老公」,都是指的同一我的,那就是我(剛纔作了個夢啦,哪有這麼好的事)。-《泡妞與設計模式》工具

這個例子很是形象地介紹了咱們平常開發中使用單例類的情景,無論在哪裏均可以得到同一個而且惟一的單例類的實例。性能

關於單例模式的簡介就到這裏,實現的細節和對模式更詳盡的介紹網上處處都是,這裏再也不浪費篇幅。

單例的模板

上一篇文章中說到的 Manager of Managers 架構,其中每一個 Manager 在 QFramework 中都是由單例實現,固然也可使用靜態類實現,可是相比於靜態類的實現,單例更爲合適。

如何設計這個單例的模板?

先分析下需求,當設計一個 Manager 時候,咱們但願整個程序只有一個該 Manager 類的實例,通常立刻能想到的實現是這樣的:

public class XXXManager 
{
    private static XXXManager instance = null;

    private XXXManager
    {
        // to do ...
    }

    public static XXXManager()
    {
        if (instance == null)
        {
            instance = new XXXManager();
        }

        return instance;
    }
}

若是一個遊戲須要10個各類各樣的 manager,那麼以上這些代碼要複製粘貼好多遍。重複的代碼太多!!! 想要把重複的代碼抽離出來,怎麼辦?

答案是引入泛型。

實現以下:

namespace QFramework 
{  
    public abstract class Singleton<T> where T : Singleton<T>
    {
        protected static T mInstance = null;

        protected Singleton()
        {
        }

        public static T Instance
        {
            get 
            {
                if (mInstance == null)
                {
                    // 如何 new 一個T???
                }

                return mInstance;
            }
        }
    }
}

爲了能夠被繼承,靜態實例和構造方法都使用了 protect 修飾符。以上的問題很顯而易見,那就是不能 new 一個泛型(2016 年 3月9日補充:並非不能new一個泛型,參考:new 一個泛型的實例,編譯失敗了,爲何?-CSDN論壇-CSDN.NET-中國最大的IT技術社區),(2016 年 4月5日補充:有同窗說能夠new一個泛型的實例,不過要求改泛型提供了 public 的構造函數,好吧,這裏不用new的緣由是,沒法顯示調用 private 的構造函數)。由於泛型自己不是一個類型,那該怎麼辦呢?答案是使用反射。

這部分之後可能會複用,因此抽出了 SingletonCreator.cs,專門用來經過反射建立私有構造示例。

實現以下:

SingletonCreator.cs

namespace QFramework
{
    using System;
    using System.Reflection;

    public static class SingletonCreator
    {
        public static T CreateSingleton<T>() where T : class, ISingleton
        {
            // 獲取私有構造函數
            var ctors = typeof(T).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
            
            // 獲取無參構造函數
            var ctor = Array.Find(ctors, c => c.GetParameters().Length == 0);

            if (ctor == null)
            {
                throw new Exception("Non-Public Constructor() not found! in " + typeof(T));
            }

            // 經過構造函數,常見實例
            var retInstance = ctor.Invoke(null) as T;
            retInstance.OnSingletonInit();

            return retInstance;
        }
    }
}

但願在單例類的內部得到初始化事件因此定製了 ISingleton 接口用來接收單例初始化事件。

ISingleton.cs

namespace QFramework
{    
    public interface ISingleton
    {        
        void OnSingletonInit();
    }
}

Singleton.cs

namespace QFramework
{
    public abstract class Singleton<T> : ISingleton where T : Singleton<T>
    {
        protected static T mInstance;

        static object mLock = new object();

        protected Singleton()
        {
        }

        public static T Instance
        {
            get
            {
                lock (mLock)
                {
                    if (mInstance == null)
                    {
                        mInstance = SingletonCreator.CreateSingleton<T>();
                    }
                }

                return mInstance;
            }
        }

        public virtual void Dispose()
        {
            mInstance = null;
        }

        public virtual void OnSingletonInit()
        {
        }
    }
}

以上就是最終實現了,而且加上了線程鎖,並且實現了一個用來接收初始化事件的接口 ISingleton。這個實現是在任何 C# 程序中都是通用的。其測試用例以下所示:

using QFramework;  
// 1.須要繼承 Singleton。
// 2.須要實現非 public 的構造方法。
public class XXXManager : Singleton<XXXManager> 
{  
    private XXXManager() 
    {
        // to do ...
    }
}


public static void main(string[] args)  
{
    XXXManager.Instance.xxxyyyzzz();
}

小結:

這個單例的模板是平時用得比較順手的工具了,其實現是在其餘的框架中發現的,拿來直接用了。反射的部分可能會耗一些性能,可是第一次調用只會執行一次,因此放心。在 Unity 中可能會須要繼承 MonoBehaviour 的單例,由於不少遊戲可能會只建立一個 GameObject,用來獲取 MonoBehaviour 的生命週期,這些內容會再下一節中介紹:)。

MonoBehaviour 單例的模板

上一小節講述瞭如何設計 C# 單例的模板。也隨之拋出了問題:

  • 如何設計接收 MonoBehaviour 生命週期的單例的模板?

如何設計?

先分析下需求:

  • 約束腳本實例對象的個數。
  • 約束 GameObject 的個數。
  • 接收 MonoBehaviour 生命週期。
  • 銷燬單例和對應的 GameObject。

首先,第一點,約束腳本實例對象的個數,這個在上一篇中已經實現了。 可是第二點,約束 GameObject 的個數,這個需求,尚未思路,只好在遊戲運行時判斷有多少個 GameObject 已經掛上了該腳本,而後若是個數大於1拋出錯誤便可。 第三點,經過繼承 MonoBehaviour 實現,只要覆寫相應的回調方法便可。 第四點,在腳本銷燬時,把靜態實例置空。 完整的代碼就以下所示:

using UnityEngine;

/// <summary>
/// 須要使用Unity生命週期的單例模式
/// </summary>
namespace QFramework 
{  
    public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T>
    {
        protected static T mInstance = null;

        public static T Instance()
        {
            if (mInstance == null)
            {
                mInstance = FindObjectOfType<T>();

                if (FindObjectsOfType<T>().Length > 1)
                {
                    Debug.LogError("More than 1!");
                    return instance;
                }

                if (instance == null)
                {
                    string instanceName = typeof(T).Name;
                    Debug.Log ("Instance Name: " + instanceName); 
                    GameObject instanceGO = GameObject.Find(instanceName);

                    if (instanceGO == null)
                        instanceGO = new GameObject(instanceName);
                        
                    instance = instanceGO.AddComponent<T>();
                    DontDestroyOnLoad(instanceGO);  //保證明例不會被釋放
                    Debug.Log ("Add New Singleton " + mInstance.name + " in Game!");
                }
                else
                {
                    Debug.Log("Already exist: " + mInstance.name);
                }
            }

            return mInstance;
        }


        protected virtual void OnDestroy()
        {
            mInstance = null;
        }
    }
}

這樣一個獨立的 MonoSingleton 就實現了。

小結:

目前已經實現了兩種單例的模板,一種是須要接收 MonoBehaviour 生命週期的,一種是不須要接收生命週期的 C# 單例的模板,能夠配合着使用。雖然不是本人實現的,可是用起來但是超級爽快,2333。

Singleton Property

文章寫到這,咱們已經實現了 C# 單例的模板和 MonoBehaviour 單例的模板,這兩個模板已經能夠知足大多數實現單例的需求了。可是偶爾仍是會遇到比較奇葩的需求的。

好比這樣的需求:

  • 單例要繼承其餘的類,好比 Model.cs 等等。

雖然單例繼承其餘類是比較髒的設計,可是不免會遇到不得不繼承的時候。沒有最好的設計,只有最合適的設計。

解決方案:

  • 首先要保證明現單例的類從使用方式上應該不變,仍是
XXX.Instance.ABCFunc();

以前的單例的模板代碼以下所示:

namespace QFramework
{
    public abstract class Singleton<T> : ISingleton where T : Singleton<T>
    {
        protected static T mInstance;

        static object mLock = new object();

        protected Singleton()
        {
        }

        public static T Instance
        {
            get
            {
                lock (mLock)
                {
                    if (mInstance == null)
                    {
                        mInstance = SingletonCreator.CreateSingleton<T>();
                    }
                }

                return mInstance;
            }
        }

        public virtual void Dispose()
        {
            mInstance = null;
        }

        public virtual void OnSingletonInit()
        {
        }
    }
}

按照之前的方式,若是想實現一個單例的代碼應該是這樣的:

using QFramework;  
// 1.須要繼承QSingleton。
// 2.須要實現非public的構造方法。
public class XXXManager : QSingleton<XXXManager> 
{  
    private XXXManager() 
    {
        // to do ...
    }
}

public static void main(string[] args)  
{
    XXXManager.Instance().xxxyyyzzz();
}

若是我想 XXXManager 繼承一個 BaseManager 代碼就變成這樣了

using QFramework;  
// 1.須要繼承QSingleton。
// 2.須要實現非public的構造方法。
public class XXXManager : BaseManager 
{  
    private XXXManager() 
    {
        // to do ...
    }
}

這樣這個類就不是單例了,怎麼辦?
答案是經過 C# 的屬性器。

using QFramework;  
// 1.須要繼承QSingleton。
// 2.須要實現非public的構造方法。
public class XXXManager : BaseManager,ISingleton
{  
    private XXXManager() 
    {
        // 不建議在這裏初始化代碼
    }
    
    void ISingleton.OnSingletonInit()
    {
        // to do ...
    }
    
    public static XXXManager Instance 
    { 
        get 
        {
            return SingletonProperty<XXXManager>.Instance;
        }
    }
}

public static void main(string[] args)  
{
    XXXManager.Instance.xxxyyyzzz();
}

好了,又看到陌生的東西了,SingletonProperty 是什麼?
和以前的單例的模板很類似,貼上代碼本身品吧...

namespace QFramework
{
    public static class SingletonProperty<T> where T : class, ISingleton
    {
        private static T mInstance;
        private static readonly object mLock = new object();

        public static T Instance
        {
            get
            {
                lock (mLock)
                {
                    if (mInstance == null)
                    {
                        mInstance = SingletonCreator.CreateSingleton<T>();
                    }
                }

                return mInstance;
            }
        }

        public static void Dispose()
        {
            mInstance = null;
        }
    }
}

這樣沒法繼承的問題就解決啦。
缺點是:相比於 Singleton,SingletonProperty 在使用時候多了一次函數調用,並且還要再實現個 getter,不過問題解決啦,。

單例的利弊

在介紹單例的最佳實踐以前,咱們要先分析下單例的利弊。

首先咱們先從定義上入手。

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

就兩句話:

  1. 保證一個類僅有一個實例。
  2. 提供一個訪問它的全局訪問點。

保證一個類僅有一個實例,這個是對單例的一個需求。可是這句話沒有告訴你,這個實例何時應該去建立。而筆者所知到的建立方式通常是有兩種,第一種是在程序編譯後立刻建立,通常實現方式是在聲明靜態成員變量的時候去 new 一個實例,實現以下。

public class Test
{
    public static readonly Test Instance = new Test();
}

這種方式最簡單,也最容易實現。

第二種則第一次獲取實例時去建立,實現以下:

public class Test
{
    public static Test mInstance;
    
    public static Test Instance
    {
        get
        {
            if (mInstance == null)
            {
                mInstance = new Test();
            }
            
            return mInstance;
        }
    }
}

這種單例實現也比較常見,被稱爲懶單例模式,乘坐懶的緣由是用到的時候再去建立,這樣能夠減緩內存和 CPU 壓力。形成的風險則是,聲明週期不可控。

因此說第一個利弊是懶加載的利弊。

  • 懶加載
    • 優勢:減小內存和 CPU 壓力。
    • 缺點:聲明週期不可控。

懶加載是可用不可用的,在 Unity 開發中通常用單例的模板時候都是用懶加載的方式的。

其餘的還有 全局惟一 和 全局訪問。

全局惟一這個沒什麼好說的,單例的存在就是爲了保證全局惟一,只有個優勢吧。

提供全局訪問。提供全局訪問這個功能,優勢是方便獲取單例實例。缺點就很明顯了,在文章的開始,筆者說

不少開發者或者有經驗的老手都會建議儘可能不要用單例模式,這是有緣由的。

這個緣由就是由於全局訪問。一個實例的全局訪問會有不少風險,固然靜態類也是能夠全局訪問的。可是靜態類通常咱們用做工具或者 Helper,因此沒什麼問題。可是單例自己是一個實例,是一個對象。因此對象有的時候是有聲明週期的,而且有時候還有上下文(緩存的數據、狀態)。而有時候還須有必定特定的順序去使用 API。這些都是很是有可能的。 因此說要設計一個好的單例類,好的管理類。是對開發者要求是很是高的。不過在這裏筆者提醒一下,不是說要把單例類設計得很是好纔是徹底正確的。有的時候,咱們來不及花精力去設計,考慮周全,可是能夠完成工做,完成任務,這樣最起碼是對得起公司付的工資的,並且功能完成了,等不忙的時候能夠回來再思考的嘛,羅馬不是一天建成的,可是羅馬能夠經過一點一點迭代完成。具體要求高在哪裏,主要是符合設計模式的六大設計原則就好。

接下來筆者就貼出一個筆者認爲比較嚴格的單例類設計。

原則上是,保留單例優勢的同時,去削弱使用它的風險。

目前來看,單例使用的風險主要是全局訪問,因此削弱全局訪問就行了。筆者所分享的方式是,對外提供的 API 都用靜態 API。Instance 變量不對外提供,外部訪問只能經過靜態的 API。而內部則維護一個私有的單例實例。

代碼以下:

using System;
using QFramework;
using UnityEngine;

/// <summary>
/// 職責:
/// 1. 用戶數據管理
/// 2. 玩家數據管理
/// 3. Manager  容器: List/Dictionary  增刪改查
/// </summary>
///
///
public class PlayerData
{
    public string Username;
    
    public int Level;
    
    public string Carrer;
}


[QMonoSingletonPath("[Game]/PlayerDataMgr")]
public class PlayerDataMgr : MonoBehaviour,ISingleton
{
    private static PlayerDataMgr mInstance
    {
        get { return MonoSingletonProperty<PlayerDataMgr>.Instance; }
    }
   
    /// <summary>
    /// 對外閹割
    /// </summary>
    void ISingleton.OnSingletonInit()
    {
        mPlayerData = new PlayerData();
        // 從本地加載的一個操做

    }
    
    #region public 對外提供的 API

    public static void SavePlayerData()
    {
        mInstance.Save();
    }
    
    public static PlayerData GetPlayerData()
    {
        return mInstance.mPlayerData;
    }
    
    #endregion

    
    private PlayerData mPlayerData;

    
    private void Save()
    {
        // 保存到本地
    }
}

使用上很是乾淨簡潔:

public class TestMonoSingletonA : MonoBehaviour {

    // Use this for initialization
    private void Start()
    {
        var playerData = PlayerDataMgr.GetPlayerData();

        playerData.Level++;

        PlayerDataMgr.SavePlayerData();     
    }

    // Update is called once per frame
    void Update () {
        
    }
}

命名小建議

到這裏還要補充一下,筆者呢不太喜歡 Instance 這個命名。在命名上,不少書籍都建議用業務命名而不是用技術概念來命名。

好比 PlayerDataSaver 是業務命名,可是 SerializeHelper 則是技術命名,本質上他們兩個均可以作數據存儲相關的任務。可是 PlayerDataSaver,更適合人類閱讀。

Instance 是技術概念命名,而不是業務命名。儘可能不要讓技術概念的命名出如今 UI/邏輯層。只能夠在框架層或者插件曾出現是容許的。

以上這些是筆者的本身的觀點,不是標準的原則,你們看看就好。

今天的內容就這些,謝謝閱讀~

相關連接:

個人框架地址:https://github.com/liangxiegame/QFramework

教程源碼:https://github.com/liangxiegame/QFramework/tree/master/Assets/HowToWriteUnityGameFramework/

QFramework&遊戲框架搭建QQ交流羣: 623597263

轉載請註明地址:涼鞋的筆記http://liangxiegame.com/

微信公衆號:liangxiegame

若是有幫助到您:

若是以爲本篇教程或者 QFramework 對您有幫助,不妨經過如下方式贊助筆者一下,鼓勵筆者繼續寫出更多高質量的教程,也讓更多的力量加入 QFramework 。

筆者在這裏保證 QFramework、入門教程、文檔和此框架搭建系列的專欄永遠免費開源。以上捐助產品的內容對於使用 QFramework 的使用來說都不是必須的,因此你們不用擔憂,各位使用 QFramework 或者 閱讀此專欄 已是對筆者團隊最大的支持了。

相關文章
相關標籤/搜索