Magia singletonów

12.08.2008 13:37 in programowanie

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ę...).

Comments:

Leave comment: