Готовый перевод Game Programming Patterns / Паттерны программирования в играх: 1.5. Singleton

Эта глава - аномалия.

Любая другая глава в этой книге покажет вам как пользоваться архитектурным паттерном.

Эта глава покажет вам как не использовать его.

Вопреки благородным намерениям, паттерн Singleton, описанный Бандой Четверых, обычно больше вредит, чем помогает.

Хотя они подчеркивают, что паттерн следует использовать аккуратно, в игровой индустрии частенько это послание пропускают мимо ушей.

Однако, как и любой паттерн, применять его там, где ему не место, так же полезно, как лечить огнестрельное ранение шиной.

Из-за чрезмерности его применения, почти вся эта глава будет посвящена тому, как его избегать. Но сначала давайте взглянем на сам паттерн.

Когда индустрия почти полностью перешла с C на объектно-ориентированное программирование, встал вопрос "как мне получить экземпляр?" .

Допустим, был метод, которые нужно вызвать, но не было экземпляра объекта, который бы немедленно этот метод предоставлял.

Паттерн Singleton (другим словами, сделать такой метод глобальным)

был простым выходом.

Шаблон Singleton (Одиночка)

Шаблоны проектирования кратко формулируют Singleton таким образом:

Гарантирует, что у класса есть единственный экземпляр, и предоставляет глобальную точку доступа к нему.

Разделим его по "и" и рассмотрим каждую часть отдельно.

Ограничение класса единственным экземпляром

Бывает, что класс не может работать правильно, если существует более чем один его экземпляр.

В общем случае класс взаимодействует с внешней системой, которая содержит свое собственное глобальное состояние.

Рассмотрим класс-обертку над API файловой системы.

Файловые операции могут занимать прилично времени, поэтому наш класс выполняет их асинхронно.

Значит несколько операций могут выполняться одновременно, и следовательно они должны координироваться между собой.

Если мы вызвали запрос на создание файла, а потом еще один на удаление того же файла, наш класс должен убедиться, запросы не перекрывают друг друга.

Чтобы сделать это, вызову внутри нашей обертки требуется иметь доступ к каждой предыдущей операции.

Если бы пользователи могли неограниченно создавать экземпляры нашего класса, ни один экземпляр не имел бы способа осведомления об операции, которую начали другие экземпляры.

Введём Singleton.

Это предоставит способ для класса гарантировать на стадии компиляции, что существует только один экземпляр класса.

Предоставление глобальной точки доступа

Несколько различных систем в игре будут использовать нашу файловую оболочку: ведение логов, загрузка контента, сохранение состояния игры и т.д.

Если эти системы не могут создать собственные экземпляры нашей файловой оболочки, как они могут получить доступ к ней?

Singleton обеспечивает решение и для этого.

В дополнение к созданию единственного экземпляра, он также обеспечивает глобальный метод доступа, чтобы получить его.

Таким образом, кто угодно и когда угодно может сунуть свои лапы в наш благословенный экземпляр.

Итак, классическое представление выглядит вот так:

class FileSystem

{

public:

static FileSystem& instance()

{

// Lazy initialize.

if (instance_ == NULL)

instance_ = new FileSystem();

return *instance_;

}

private:

FileSystem() {}

static FileSystem* instance_;

};

член static instance_ содержит екземпляр класа, и приватный конструктор проверяет что он всего один.

Метод public static instance() обеспечивает доступ к экземпляру из любого места в коде.

Он также ответственен за ленивое создание экземпляра синглетона при первом запросе.

В итоге, выглядит он так:

class FileSystem

{

public:

static FileSystem& instance()

{

static FileSystem *instance = new FileSystem();

return *instance;

}

private:

FileSystem() {}

};

С++11 предписывает, что бы инициализатор для локальной статической переменной запускался лишь однажды при наличии параллелизма.

Почему мы это используем

Сохранение памяти и количества циклов CPU всегда хорошо.

Вот базовые классы.

class FileSystem

{

public:

virtual ~FileSystem() {}

virtual char* readFile(char* path)

= 0;

virtual void writeFile(char* path, char* contents)

= 0;

};

class PS3FileSystem : public FileSystem

{

public:

virtual char* readFile(char* path)

{

// Use Sony file IO API...

}

virtual void writeFile(char* path, char* contents)

{

// Use sony file IO API...

}

};

class WiiFileSystem : public FileSystem

{

public:

virtual char* readFile(char* path)

{

// Use Nintendo file IO API...

}

virtual void writeFile(char* path, char* contents)

{

// Use Nintendo file IO API...

}

};

Теперь превратим FileSystem в синглетон:

class FileSystem

{

public:

static FileSystem& instance();

virtual ~FileSystem() {}

virtual char* readFile(char* path)

= 0;

virtual void writeFile(char* path, char* contents)

= 0;

protected:

FileSystem() {}

};

FileSystem& FileSystem::instance()

{

#if PLATFORM == PLAYSTATION3

static FileSystem *instance = new PS3FileSystem();

#elif PLATFORM == WII

static FileSystem *instance = new WiiFileSystem();

#endif

return *instance;

}

Мы имеем врапер файловой системы.

Во первых, это не проблема.

Но мы не можем.

Log::instance().write("Some event."

);

class FileSystem

{

public:

static FileSystem& instance() { return instance_; }

private:

FileSystem() {}

static FileSystem instance_;

};

class Bullet

{

public:

int getX() const { return x_; }

int getY() const { return y_; }

void setX(int x)

{ x_ = x; }

void setY(int y)

{ y_ = y; }

private:

int x_, y_;

};

class BulletManager

{

public:

Bullet* create(int x, int y)

{

Bullet* bullet = new Bullet();

bullet->setX(x)

;

bullet->setY(y)

;

return bullet;

}

bool isOnScreen(Bullet& bullet)

{

return bullet.

getX() >= 0 &&

bullet.

getX() < SCREEN_WIDTH &&

bullet.

getY() >= 0 &&

bullet.

getY() < SCREEN_HEIGHT;

}

void move(Bullet& bullet)

{

bullet.

setX(bullet.

getX() + 5);

}

};

Если посмотреть на этот код, естественно поймешь что BulletManager должен быть синглтоном.

Ответ - ноль.

Вот как мы решаем проблему синглтона для нашего класса менеджера.

class Bullet

{

public:

Bullet(int x, int y)

: x_(x)

, y_(y)

{}

bool isOnScreen()

{

return x_ >= 0 && x_ < SCREEN_WIDTH &&

y_ >= 0 && y_ < SCREEN_HEIGHT;

}

void move() { x_ += 5; }

private:

int x_, y_;

};

Ну вот.

Нет менеджера, нет проблем.

Вот один из них.

class FileSystem

{

public:

FileSystem()

{

assert(!instantiated_);

instantiated_ = true;

}

~FileSystem() { instantiated_ = false; }

private:

static bool instantiated_;

};

bool FileSystem::instantiated_ = false;

class GameObject

{

protected:

Log& getLog() { return log_; }

private:

static Log& log_;

};

class Enemy : public GameObject

{

void doSomething()

{

getLog().write("I can log!"

);

}

};

class World

{

public:

static World& instance() { return instance_; }

// Functions to set log_, et.

al.

...

Log& getLog() { return *log_; }

FileSystem& getFileSystem() { return *fileSystem_; }

AudioPlayer& getAudioPlayer() { return *audioPlayer_; }

private:

static World instance_;

Log *log_;

FileSystem *fileSystem_;

AudioPlayer *audioPlayer_;

};

World::instance().getAudioPlayer().play(VERY_LOUD_BANG)

;

Переведено на Нотабеноиде
http://notabenoid.org/book/51192/215214

Переводчики: stfbee, Deo, warchief, Alkarion

Avejant, BaMnup16

Настройки

Готово:

53.27% КП = 1.0

Непереведённые фрагменты будут пропущены.