Bizonyos objektumtípusokból (egyszerre) csak egyre van szükségünk. Ezeket általában Menedzsereknek nevezzük. A Singleton egy olyan tervezési minta vagy más néven Desig Pattern, amiben egy típus esetén gondoskodunk arról, hogy az adott osztályból csak egy példány létezhessen. Mindezt úgy hogy ezen példányhoz a hozzáférést egyszerűvé és gyorssá tesszük.
Egy gyakran előforduló programozási problémára adott, általános, bevett és újrafelhasználható, megoldás.
Ezen megoldások általában nem kötődnek szorosan egy fejlesztési keretrendszerhez, azaz valamelyest platform és nyelvfüggetlenek.
A minták általában osztályok és objektumok közötti kapcsolatokat és műveleteket írnak le azok nevesítése, részletes specifikációja, és konkrét implementációja nélkül.
A Singleton pattern ezért ideális Menedzsel osztályok számára: Komponensek hierarchiája
Általános megvalósítás
A Singleton az egyik legismertebb tervezési minta és a Unity-n kívül is sokat használják. Az alábbi leírás működik minden objektumorientált nyelvben. Lényege:
- Úgy érjük el, hogy csak egyetlen példány létezhessen egy osztályból, hogy felüldefiniáljuk a paraméter nélküli konstruktorát és priváttá tesszük azt. Ezáltal kívülről senki nem fog tudni létrehozni új példányt, csak az adott osztály maga.
- Létrehozunk egy privát statikus példányt a típusból és hozzá egy publikus statikus lekérdező függvényt. Ha az egyetlen példány még nem létezik akkor, amikor valaki ezt a lekérdező függvényt hívja, akkor létrehozzuk és eltesszük a statikus változóba. Ha már ez megtörtént egyszer akkor egyszerűen csak visszaadjuk a példányt.
class SingletonExample
{
private static SingletonExample _instance; // Az egyetlen példány
private SingletonExample() { } // Paraméter nélküli konstruktor-t priváttá tesszük,
// így kívülről nem lehet példányosítani az osztályt.
// Lekérdezést itt függvénnyel valósítom meg. (Proprtyis lehetne)
public static SingletonExample GetInstance()
{
if (_instance == null) // Ha ez az első lekérdezés,
_instance = new(); // akkor létrehozzuk a példányt.
// (Itt gyakorlatilag a fenti privát felüldefiniált konstruktort hívjuk)
return _instance; // Visszaadjuk a Singleton objektumot.
}
}
// (A fenti kódban a private kulcsszavak természetesen elhagyhatók.)
A fenti kód gondoskodik arról, hogy máshonnan ne lehessen példányosítani az osztályt, de az egyetlen objektumhoz belőle könnyen hozzá lehessen férni.
// Bárhol a kódban:
SingletonExample theInstance = SingletonExample.GetInstance();
Singleton MonoBechaviour
A fenti példa kicsikét máshogy néz ki ha MonoBehaviour osztály esetén használjuk. Ezen osztályokból nem úgy hozunk létre példányt, hogy kódból használjuk a new
kulcsszót, hanem az Editorban Add Component lehetőséggel hozzáadjuk őket GameObject-ekhez. Ezért MonoBechaviour szkriptekre egy kicsit át kell szabni a singleton-t.
A Unity-t nem érdekli, hogy egy komponens konstruktora privát vagy publikus. Bizonyos trükkökkel lehet privát függvényeket is hívni kívülről. A Unity is így tesz. Sajnos emiatt nem tudjuk meggátolni, hogy több példány párhuzamosan létezzen egy komponensből, de tudunk hibaüzenetet dobni ekkor.
A Singleton MonoBechaviour általános kódja:
class SingletonExample : MonoBehaviour
{
private static SingletonExample _instance; // Az egyetlen példány
void Awake() // Ha létrejön az objektum, és...
{
if (_instance != null) // már létezik egy másik, akkor...
Debug.LogError("SingletonExample already exists!"); // hibát jelzünk!
else // Egyébként...
_instance = this; // elmentjük az objektumot statikus mezőbe.
}
public static SingletonExample GetInstance()
{
// Ha még nem kötöttük be példányt a statikus mezőbe, akkor
// Megpróbáljuk megkeresni a jelenlegi játékban.
if (_instance == null)
_instance = FindObjectOfType<SingletonExample>();
// Ez akkor történhet meg, ha a lekérdező Awake-je hamarabb fut le,
// mint a Singleton osztályé és bebben az Awake-ben kísérelték meg a legérdezést
// Ha nem találtuk meg a Singleton objektumot, akkor ...
if (_instance == null)
Debug.LogError("SingletonExample not found!"); //hibát jelzünk!
return _instance; // Visszaadjuk a Singleton objektumot.
}
}
A fenti kód csak egy megvalósítsa a Singleonnek. Több variáció elképzelhető belőle.
- Elhagyható a duplikáns példány tesztelés Awake-ben
- Elhagyhatóm a
FindObjectOfType
keresés lekérdezésnél - Elhagyható a null teszt lekérdezésnél
- Kibővíthető a kód azzal, hogy automatikusan történjen meg a létrehozása a komponensnek, a még nem létezik.
Singleton MonoBechaviour-ok azért nagyon hasznosak, mert úgy tudunk menedzser osztály készíteni általuk, hogy a Unity hívja annak üzenetmetódusait: Update()
, FixedUpdate()
…
Singleton MonoBechaviour megvalósítása, ősosztály-jal
A fenti kód megvalósítása kissé körülményes minden egyes Singleton esetén, ezért intézhetjük a munka nagy részét egy ősosztályban is.
Az alábbiak megértéséhez szükséges az Objektum orientáltság és öröklés (Félkész) és a Generikusok (Hamarosan) leckék megértése.
using UnityEngine;
public class SingletonMonoBehaviour<T> : MonoBehaviour where T: MonoBehaviour
{
static T _instance;
void Awake()
{
if (_instance != null)
Debug.LogError("SingletonExample already exists!");
else
_instance = (T)(object)this; // szükséges a kettő kasztolás
}
OnSingletonAwake(); // Mivel itt megvalósítjuk az Awake-et, csinálunk egy alternatív
// verziót, hogy a leszármazott oszályok is tudják használni
}
public static T Instance // A lekérdezést most property-vel végzem.
{
get
{
if (_instance == null)
_instance = FindObjectOfType<T>();
return _instance;
}
}
protected virtual void OnSingletonAwake() { } // Leszármazottaknak ez az Awake
}
Használata: Most definiáljunk egy Singleton MonoBehaviour-t az ősosztály segítségével:
class MyClass : SingletonMonoBehaviour<MyClass>
{
protected sealed override void OnSingletonAwake() { } // Awake helyett
// ...
}
Ilyen egyszerű!
Singleton vs. FindObjectOfType
De mi értelme Singleton-t használni amivel le tudjuk kérni egy MonoBehaviour objektum egyetlen példányét? Nem lenne ugyanilyen egyszerű a FindObjectOfType
használata?
De. FindObjectOfType
-ot ugyanolyan könnyen lehetne használni, ám az sokkal lassabb. Ahogy a neve is utál rá ez a művelet végrehajt egy keresést, ami időigényes és ideje függ az objektumok számától. Ezzel szemben egy hozzáférés egy statikus változóhoz nagyon gyors és fix idejű nem befolyásolja az objektumok száma.
Ezért gyakran FindObjectOfType
-ot csak egyszer használjuk egy szkripben Start()
vagy Awake()
híváson belül, amikor is elmentjük ezt a menedzser referenciát egy mezőbe, a későbbi használatra. Ez a módszer valóban nagyban csökkenti az összes lekérdezési időt, hisz így csak egyszer tesszük meg egy objektumra. Mindazonáltal ki kell emelni, hogy ez csak akkor biztosít stabil eredményt, ha sosem cseréljük le futás közben ezt a menedzsert. Továbbá ebben az esetben nem tudunk olyan egyéb logikákat hozzáadni egy lekérdezéshez, mint nullteszt vagy automatikus létrehozás.
A FindObjectOfType
előnye a Singeton-hoz képest az, hogy vele képesek vagyunk interface vagy absztrakt osztály alapján lekérni típusokat. A Singeton tervezési mintával ez nem megoldható, viszont elkészíthetünk bonyolultabb rendszereket is, amik egyszerre gyorsak és támogatják az absztrakciót.
Kapcsolódó: Interface-ek (Hamarosan), Absztrakció, Polimorfizmus (Hamarosan)
ServiceLocator (Kiegészítő anyag)
Akkor lehet hasznos egy menedzsert interface szerint elérni, ha egy projekten belül több implementációja is létezik.
Pl.: Bizonyos scene-ekben más-más a TimeManager: LimitedTimeManager
, EndlessTimeManager
, TutorialTimeManager
. Ezek mindegyike megvalósítja az ITimeManager
interface-t
Ekkor azt szeretnénk elérni, hogy ha valaki valamikor lekéri az ITimeManager
objektumot, akkor mindig az aktuálisan létezőt kapja meg.
Egy másik tervezési mintával, az úgy nevezett Service Locator MonoBehaviour-ra szabott változatával elérhető ez. Vele bármilyen menedzser típus regisztrálni tudja magát akár ősosztálya vagy interface-ei szerint, úgy hogy az később bárhonnan elérhető.
statikus osztály egy Dictionary-jában eltárolhatjuk a menedzser típusainkat interface-ek szerint.
Dictionary-ról bővebben: Egyéb adatszerkezetek
static class ServiceLocator
{
static readonly Dictionary<Type, object> services = new();
public static void Register<T>(T service)
{
Type type = typeof(T);
Type mainType = service.GetType();
if (!mainType.IsSubclassOf(type) && !mainType.GetInterfaces().Contains(type))
{
Debug.LogError($"Service type {mainType} must implement {type}");
return;
}
if (services.ContainsKey(type))
{
if (services[type] != null)
{
Debug.LogError($"Service of type {type} already registered");
return;
}
else
services[type] = service;
}
else
services.Add(type, service);
}
public static T Get<T>() where T : class
{
if (services.TryGetValue(typeof(T), out object service))
return (T)service;
else
return null;
}
}
Használat:
// Típus definíciók és regisztráció a ServiceLocator-ba:
interface IExampleService1 { /* ... */ }
interface IExampleService2 { /* ... */ }
class ServiceComponent : MonoBehaviour, IExampleService1, IExampleService2
{
void Awake()
{
// Regisztráljuk magunkat az interface-űnkön keresztül
ServiceLocator.Register<IExampleService1>(this);
ServiceLocator.Register<IExampleService2>(this);
}
}
//Lekérdezés:
IExampleService1 myInterface = ServiceLocator.Get<IExampleService1>();
ServiceComponent myObject = ServiceLocator.Get<ServiceComponent>(); // ERROR