[C#/Unity] 回顧所使用過的 Singleton 實作方式 – Difference of Four Singleton practicing
Singleton 是個充滿爭論的設計模式,從某些角度來看,Singleton 並不完全符合物件導向設計原則的所有理念,但是 Singleton 又某種程度上的不易被取代。所以一般來說的建議是:少用、小心地用。
過去我曾設計兩套實作方式,企圖用集合管理的方式,完全取代單一 Singleton 的使用:
- [Unity] 應用 Singleton pattern 及 Unity Component 做系統拆分與管理 – Dividing your game system in unity
- [C#/Unity] 更多 Singleton – More Singleton in Unity
不過最近我又把單一 Singleton 撿回來使用了,並回顧了我所遭遇過的使用情境,對何時該用何種 Singleton 實作,立下一套主觀原則。
我在 Unity 使用過的四種 Singleton 實作:
- 單一獨立的 Singleton 類別
- 單一獨立的 MonoBehaviour 與其綁定的 DontDestroy 物件
- 單一的 MonoBehaviour 註冊系統與其綁定的 DontDestroy 物件
- 單一的介面(interface) 註冊系統與其依賴的單一的 MonoBehaviour、綁定的 DontDestroy 物件
單一獨立的 Singleton 類別
1 | using UnityEngine; |
相當簡易的一個方式,只要類別繼承了 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 實作,我自己訂下的依賴原則是:可以向上層依賴,但要盡力避免向下層依賴。
而層與層的關係如下: