[C#/Unity] 回顧所使用過的 Singleton 實作方式 – Difference of Four Singleton practicing

Singleton 是個充滿爭論的設計模式,從某些角度來看,Singleton 並不完全符合物件導向設計原則的所有理念,但是 Singleton 又某種程度上的不易被取代。所以一般來說的建議是:少用、小心地用。

過去我曾設計兩套實作方式,企圖用集合管理的方式,完全取代單一 Singleton 的使用:

不過最近我又把單一 Singleton 撿回來使用了,並回顧了我所遭遇過的使用情境,對何時該用何種 Singleton 實作,立下一套主觀原則。

我在 Unity 使用過的四種 Singleton 實作:

  • 單一獨立的 Singleton 類別
  • 單一獨立的 MonoBehaviour 與其綁定的 DontDestroy 物件
  • 單一的 MonoBehaviour 註冊系統與其綁定的 DontDestroy 物件
  • 單一的介面(interface) 註冊系統與其依賴的單一的 MonoBehaviour、綁定的 DontDestroy 物件

單一獨立的 Singleton 類別

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;

public class Singleton<T> where T : class, new() {
private static object m_oLock = new object ();
private static T m_oInstance = null;
public static T Instance {
get {
lock (m_oLock) {
if (m_oInstance == null) {
m_oInstance = new T ();
}
return m_oInstance;
}
}
}
}

相當簡易的一個方式,只要類別繼承了 Singleton,便可以擁有 Singlton pattern 的效果,並且有簡單的 thread-safe 功能。

優點

  • 相當簡單易用,隨時可以在專案中大量建立 Singlton pattern (不過當然不建議如此做),輕量且不依賴其他條件。
  • 只有在第一次被使用的時候會進行實體化,雖然時機點不可控,但可以保證需要的時候一定有實體可呼叫。

缺點

  • 整個專案的穩定性 (thread-safe等) 與耦合性 (code coupling) 完全取決於後續的實作方式,容易在不知不覺間過度濫用。
  • 繼承關係會因此實作方式受限,不易沿用部分代碼或進行彈性設計。

使用情境

對於單一的工具,與其他功能不會有主動的互動,而且在專案中具有唯一、貫穿整個專案的特性時,可以考慮使用。目前我用於:事件的註冊管理、專案Assets資源的管理等獨立工具上。

** 另外為了避免 thread-safe 的錯誤,我會避免在這個類型的 Singleton 中使用 Unity API。

單一獨立的 MonoBehaviour 與其綁定的 DontDestroy 物件

詳細作法可以參考:http://wiki.unity3d.com/index.php/Singleton

這個作法是上個實作的 MonoBehaviour 版本,是在 Unity 的架構下誕生的特殊存在,會自動產生所需要的 GameObject 進行掛載。

優點

類似一般 Singlton 簡單易用,雖然我只使用過一次。

缺點

因為 Unity 本身是個特殊的架構,所以依附在這架構下的 Singleton 變得相當複雜且不易控。如果許多 Singleton 都有自己的 GameObject,我覺得還蠻討厭的。

使用情境

應該不打算再次用在專案開發上,可能的使用情境就是獨立於專案外,為了可以輕易複製到其他專案使用的小型工具。

MonoBehaviour 為基底的註冊系統

我將這系統的基底類別命名為 GameSystemMono,是在 [Unity] 應用 Singleton pattern 及 Unity Component 做系統拆分與管理 – Dividing your game system in unity 中介紹過的一套實作,算是對上一個實作進行設計的替代方案。

優點

  • 只有一個 GameObject 作為掛載個體,利用管理器進行各個 Singleton 系統(GameSystemMono) 的添加、移除、呼叫等動作,也因此可以控制每個 Singleton 之間的初始化順序。
  • 配合 Unity 的 Inspector,可以對每個系統保留作為設定的參數欄位,也可以在 Editor 即時監控數值變化,是保留 Unity 編輯器優點的一個方案。
  • 因為有 MonoBehaviour 為基底,所以除了添加及移除,還可以暫時停止系統 (SetActive為 false),有著額外的可控性。

缺點

  • 因為多了一個註冊添加的動作,不能保證隨時可以呼叫,開發時要先行規劃整套系統的建構、呼叫次序。
  • 繼承上相當受限,無法與其他插件或特別設計的 MonoBehaviour 子類別合併使用。
  • 呼叫上程式碼會較冗長一點,並且為了避免不斷重複查詢的動作,會需要像 Component 一般自行暫存的動作。
  • 沒有實作 thread-safe。

使用情境

目前作為我的專案大架構之一,許多與 Unity 的互動與設計會應用單一 MonoBehaviour 來管理其他的複數物件,便會將之註冊於 GameSystemMonoManager 之中。

一般來說一個遊戲的設計,可能會再細分成許多子系統,通常一個子系統我便會包裝成一個 GameSystemMono 作為單一的功能呼叫窗口,避免系統之下的許多物件或腳本相互耦合。

至於每個子系統的回饋與互動,則盡量採用事件的方式來進行傳遞,事件的註冊與管理使用上述的第一種 Singleton 來實作。

介面 (interface) 為基底的註冊系統

我命名為 GameModule,是在 [C#/Unity] 更多 Singleton – More Singleton in Unity 所實作的一套系統,用來與 GameSystemMono 互補。

優點

  • 嚴格來說不需要任何掛載實體,不過為了使用方便還是會依賴一個 GameObject 。
  • 基底部使用類別而用介面 (interface),所以不會影響到其他的繼承關係設計。
  • 可以透過擴充不同的介面實作得到不同功能,例如 Update、LateUpdate、SlowUpdate、Sleep 等,使用彈性比 GameSystemMono 高。

缺點

  • 與 GameSystemMono 相同,需要註冊添加的動作,不能保證隨時可以呼叫。
  • 呼叫上程式碼會較冗長一點,並且為了避免不斷重複查詢的動作,會需要像 Component 一般自行暫存的動作。
  • 無法透過 Inspector 進行彈性控制。
  • 沒有實作 thread-safe。

使用情境

GameModule 的使用情境我設定為:一個完整且相對獨立的功能,不太需要因為專案變化而進行調整的模塊,除了被動呼叫也會有主動的回饋,是與上述的第一種 Singleton 實作最大差別。

目前我所設計的 GameModule 有場景切換管理整個遊戲的流程控制網路連線與封包的控制等比較通用於專案之間的功能。

小結

雖然說 Singleton pattern 容易呼叫且應該避免耦合,但是要完全不耦合是相當困難的,因此對於這幾個 Singleton 實作,我自己訂下的依賴原則是:可以向上層依賴,但要盡力避免向下層依賴。

而層與層的關係如下:

singleton-class