Ze smutkiem stwierdziłem, że pisząc silnik grafiki 3D padłem ofiarą nieprzemyślanych wzorców projektowych. Mówię tu o zastosowaniu singletonów do większości elementów silnika. Pisząc takowy, nie powinniśmy się martwić o skomplikowane zależności między klasami czy wielokrotną enkapsulację, a raczej o szybkość działania. Z tego powodu czasem warto, aby jedna klasa mogła po prostu wywołać sobie funkcję innej klasy. A ponieważ w grze wideo z definicji większość klas ma tylko jeden egzemplarz (klasy systemów: graficznych, fizycznych, etc), to ,,upiększenie'' kodu przez singletony samo się prosi. Zobaczmy:
void CPlayer::Hurt()
{
CGame::DecreaseLives();
CHUD::NotifyUpdate();
CSound::PlaySound("hurt", GetPosition());
}
I tak dalej, i tak dalej. Fajnie jest operować na klasach, korzystając po prostu z NazwaKlasy::, ale zadajmy sobie pytanie: za jaką cenę? Singletony zaimplementowałem u siebie mniej więcej w taki sposób:
class CCoolSingleton
{
static CCoolSingleton* instance;
static CCoolSingleton* GetInstance();
CCoolSingleton();
int __DoVeryFunnyThing(int param, double another, const string &andmore);
public:
void DoVeryFunnyThing(int param, double another, const string &andmore)
{
CCoolSingleton::GetInstance()->__DoVeryFunnyThing(param, another, andmore);
}
};
Po co dodatkowa funkcja? Żeby nie pisać w kodzie w co drugiej linijce XXX::GetInstance()->, bo zmniejsza to jego czytelność poniżej jakichkolwiek norm. Efekt jest z grubsza identyczny, bo funkcja bez __ jest inline.
Co z tego wszystkiego mamy?
- na pewno nie stworzymy więcej niż jednej instancji
To jest niewątpliwe, ale czy tego typu ,,zabezpieczenie'' jest bardzo przydatne? - gdy nie użyjemy klasy ani razu, jej egzemplarz nigdy nie zostanie stworzony
A to zysk niewątpliwy, prawda? Ciśnie się tylko pytanie: skoro nigdy nie zostanie stworzony, to po co w ogóle tworzyć taką klasę? - nie zaśmiecamy globalnej przestrzeni nazw
Tak jakby po #include <windows.h> globalna przestrzeń nazw nie wyglądała jak galaktyka po wybuchu supernowej
Na szczęście człowiek z wiekiem wyrasta z pewnych rzeczy. Zobaczmy taki kod:
class CCoolSingleton
{
static void Init();
CCoolSingleton();
public:
void DoVeryFunnyThing(int param, double another, const string &andmore);
};
extern CCoolSingleton *pCoolSingleton;
Czy nie cudowne? W kodzie zamiast
CCoolSingleton::DoVeryFunnyThing(69, 0.666, "Jackdaws love my big sphinx of quartz");
mamy
pCoolSingleton->DoVeryFunnyThing(69, 0.666, "Jackdaws love my big sphinx of quartz");
Różnica niewielka, a wydajność rośnie. Konieczność pamiętania o Init() nie jest wadą, bo przecież obiekty zazwyczaj tak czy inaczej trzeba jakoś zainicjalizować (Init ma coś w rodzaju pCoolSingleton = new CCoolSingleton()).
A jeżeli bardzo chcemy globalną przestrzeń nazw odkurzyć, to można przecież wszystkie takie "singletony" zapakować do jednej struktury, która będzie trzymać ich wskaźniki (jako zmienne składowe, bez używania map ani hash-table, proszę...).