Как я могу избежать множества синглтонов в моей игровой архитектуре?

Я использую движок cocos2d-x для создания игр. Двигатель уже использует много синглтонов. Если кто-то использовал его, то они должны быть знакомы с некоторыми из них:

Director
SimpleAudioEngine
SpriteFrameCache
TextureCache
EventDispatcher (was)
ArmatureDataManager
FileUtils
UserDefault

и многие другие, в общей сложности около 16 классов. Вы можете найти аналогичный список на этой странице: Одиночные объекты в Cocos2d-html5 v3.0 Но когда я хочу написать игру, мне нужно гораздо больше синглонов:

PlayerData (score, lives, ...)
PlayerProgress (passed levels, stars)
LevelData (parameters per levels and level packs)
SocialConnection (Facebook and Twitter login, share, friend list, ...)
GameData (you may obtain some data from server to configure the game)
IAP (for in purchases)
Ads (for showing ads)
Analytics (for collecting some analytics)
EntityComponentSystemManager (mananges entity creation and manipulation)
Box2dManager (manages the physics world)
.....

Почему я думаю, что они должны быть одиночными? Потому что я буду нуждаться в них в самых разных местах в моей игре, и общий доступ будет очень удобен. Другими словами, я не собираюсь создавать их где-то и передавать указатели на мою архитектуру, так как это будет очень сложно. Также это то, что мне нужно только одному. В любом случае мне понадобятся несколько, я тоже могу использовать шаблон Multiton. Но самое худшее, что Синглтон является наиболее сильно критикованным шаблоном из-за:

 - bad testability
 - no inheritance available
 - no lifetime control
 - no explicit dependency chain
 - global access (the same as global variables, actually)
 - ....

Вы можете найти здесь несколько соображений: https://stackoverflow.com/questions/137975 /what-is-so-bad-about-singletons и https://stackoverflow.com/questions/4074154/when-should-the-singleton-pattern-not-be-used-besides-the-obvious

Итак, я думаю, я делаю что-то неправильно. Я думаю, что мой код пахнет . :) Я понял, как более опытные разработчики игр решают эту архитектурную проблему? Я хочу проверить, может быть, в разработке игр по-прежнему нормально иметь более 30 синглетов , которые считаются теми, которые уже находятся в игровом движке.

Я думал использовать Singleton-Facade, который будет иметь экземпляры всех этих классов, которые мне нужны, но каждый из них уже не будет синглотами. Это устранит множество проблем, и у меня будет только один синглтон, который будет сам Фасад. Но в этом случае у меня будет другая проблема дизайна. Фасад станет ОБЪЕКТОМ БОГА. Я думаю, что этот запах тоже . Поэтому я не могу найти хорошее решение для этой ситуации. Пожалуйста, совет.

52 голоса | спросил Narek 30 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowTue, 30 Sep 2014 16:15:43 +0400 2014, 16:15:43

8 ответов


12

Прочитав все эти ответы, комментарии и статьи, особенно эти две блестящие статьи,

В конце концов, я пришел к следующему выводу, который является своего рода ответом на мой собственный вопрос. Лучший подход - не быть ленивым и передавать зависимости напрямую. Это явный, проверяемый, глобальный доступ отсутствует, может указывать на неправильный дизайн, если вы должны передавать делегаты очень глубоко и во многих местах и ​​так далее. Но в программировании один размер не подходит всем. Таким образом, все еще есть случаи, когда передать их напрямую нецелесообразно. Например, если мы создадим игровую среду (шаблон кода, который будет повторно использоваться в качестве базового кода для разработки различных видов игр), и включите там множество услуг. Здесь мы не знаем, насколько глубока каждая услуга может быть передана, и сколько мест она будет необходима. Это зависит от конкретной игры. Итак, что мы делаем в подобных случаях? Мы используем шаблон проектирования Service Locator вместо Singleton, который на самом деле является одним и тем же Singleton, со следующими очень важными улучшениями:

  1. Вы не программируете реализации, а интерфейсы. Это очень важно с точки зрения директоров ООП. Во время выполнения вы можете изменить или даже отключить службу, используя шаблон Null Object. Это не только важно с точки зрения гибкости, но и непосредственно помогает в тестировании. Вы можете отключить, высмеять, также можете использовать шаблон Decorator, чтобы добавить некоторые записи и другие функции. Это очень важное свойство, которого нет у Синглтона.
  2. Еще одно огромное преимущество заключается в том, что вы можете контролировать время жизни объекта. В Синглтоне вы контролируете только время, в течение которого объект будет порожден шаблоном Lazy Initialization. Но как только объект будет порожден, он будет жить до конца программы. Но в некоторых случаях вы хотели бы изменить сервис. Или даже выключить его. Особенно в играх эта гибкость очень важна.
  3. Он объединяет /обматывает несколько Singletons в один компактный API. Я думаю, вы также можете использовать некоторые Facade, такие как Service Locators, которые для одной операции могут использовать сразу несколько служб.
  4. Вы можете утверждать, что у нас все еще есть глобальный доступ, который является основной проблемой проектирования с Singleton. Я согласен, но нам нужен только этот шаблон и только если мы хотим получить глобальный доступ. Мы не должны жаловаться на глобальный доступ, если считаем, что прямая инъекция зависимостей не подходит для нашей ситуации, и нам нужен глобальный доступ. Но даже для этой проблемы есть улучшение (см. второй статья выше). Вы можете сделать все частные услуги, и вы можете наследовать из класса Service Locator все классы, которые, по вашему мнению, должны использовать эти сервисы. Это может значительно сократить доступ. И, пожалуйста, учтите, что в случае Singleton вы не можете использовать наследование из-за частного конструктора.

Также следует учитывать, что эта модель использовалась в Unity, LibGDX, XNA. Это не является преимуществом, но это своего рода доказательство удобства использования шаблона. Считайте, что эти двигатели были разработаны многими умными разработчиками долгое время, и на этапах эволюции двигателя они все еще не нашли лучшего решения.

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

EDIT: после года работы и использования прямого DI и проблем с огромными конструкторами и большим количеством делегаций я провел больше исследований и нашел хорошую статью, в которой рассказывается о плюсах и минусах методов DI и Service Locator. Ссылаясь на эту статью ( http://www.martinfowler.com/articles/injection.html ) Мартина Фаулера, который объясняет, когда на самом деле локаторы службы плохи:

  

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

Но в любом случае, если вы хотите впервые в первый раз, вы должны прочитать это закону Деметры (LoD) или принципу наименьшего знания .

ответил Narek 1 +04002014-10-01T10:56:25+04:00312014bEurope/MoscowWed, 01 Oct 2014 10:56:25 +0400 2014, 10:56:25
39

Я инициализирую свои службы в своем основном классе приложения, а затем передаю их в качестве указателей на все, что нужно использовать, либо через конструкторы, либо в функции. Это полезно по двум причинам.

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

Два, понятно, кто зависит от чего. Я легко вижу, какие классы нужны для каких-либо служб только из объявления класса.

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

ответил megadan 30 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowTue, 30 Sep 2014 19:43:35 +0400 2014, 19:43:35
17

Я не буду обсуждать о зле за одиночками, потому что Интернет может сделать это лучше меня.

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

Концепция довольно проста. У вас есть только один синглтон, который действует как единственный интерфейс, чтобы достичь того, что вы использовали в качестве Singleton. Вместо того, чтобы иметь несколько Singleton, у вас теперь есть только один.

Вызов:

FlyingToasterManager.GetInstance().Toast();
SmokeAlarmManager.GetInstance().Start();

Похож на это, используя шаблон Locator службы:

SvcLocator.FlyingToaster.Toast();
SvcLocator.SmokeAlarm.Start();

Как вы можете видеть, каждый синглтон содержится в локаторе сервисов.

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

[ EDIT ]: как указано в комментариях, сайт gameprogrammingpatterns.com также содержит очень интересный раздел о Singleton .

Надеюсь, это поможет.

ответил lvictorino 30 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowTue, 30 Sep 2014 17:04:50 +0400 2014, 17:04:50
3

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

Программисты часто полагаются на шаблон Singleton как средство удобства и чистой лени, вместо того, чтобы брать альтернативный подход и быть более подробными и внушительными объектными отношениями друг с другом в явной усадьбе.

Так спросите себя, что легче поддерживать.

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

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

Как отмечали другие, использование шаблона Singleton также приводит к запутыванию связи, что часто означает плохой дизайн и высокую степень сцепления между модулями. Такая когезия обычно нежелательна и приводит к хрупкому коду, который трудно поддерживать.

ответил Naros 1 +04002014-10-01T08:08:26+04:00312014bEurope/MoscowWed, 01 Oct 2014 08:08:26 +0400 2014, 08:08:26
1

Синглтон - известный образец, но хорошо знать цели, которые он обслуживает, и его плюсы и минусы.

Это действительно имеет смысл, если вообще существует no relationship .
Если вы можете обрабатывать свой компонент с совершенно другим объектом (без сильной зависимости) и ожидать того же поведения, синглтон может быть хорошим выбором.

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

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

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

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

ответил Crazyrems 1 +04002014-10-01T13:14:20+04:00312014bEurope/MoscowWed, 01 Oct 2014 13:14:20 +0400 2014, 13:14:20
1
  

В двигателе уже используется много синглтонов. Если кто-то использовал это, то они   должны быть знакомы с некоторыми из них:

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

Существует много веских причин, по которым Синглтон необходим или неизбежен. Рамки игр часто используют одиночные игры, потому что это неизбежное следствие наличия только одного аппаратного обеспечения с состоянием. Нет смысла когда-либо хотеть контролировать эти аппаратные средства несколькими экземплярами своих соответствующих обработчиков. Графическая поверхность - это внешнее аппаратное обеспечение, и случайная инициализация второй копии графической подсистемы - это не что иное, как катастрофическое, так как теперь две графические подсистемы будут сражаться друг с другом, кто получает возможность рисовать и когда, переписывая друг друга неуправляемо. Аналогично, с системой очереди событий, они будут бороться, кто получает события мыши недетерминированным способом. Когда речь идет о внешнем внешнем оборудовании, в котором есть только один из них, singleton неизбежно предотвращает конфликты.

Другое место, где Синглтон разумно, - это менеджеры Cache. Кэши - особый случай. Они не должны считаться синглтоном, даже если они используют все те же методы, что и синглтоны, чтобы оставаться в живых и жить вечно. Службы кэширования - это прозрачный сервис, они не должны изменять поведение программы, поэтому, если вы замените службу кеша нулевым кэшем, программа все равно должна работать, за исключением того, что она работает только медленнее. Основная причина, по которой менеджеры кэша являются исключением из singleton, заключается в том, что не имеет смысла закрывать службу кеша до выключения самого приложения, поскольку оно также отбрасывает кешированные объекты, что наносит ущерб тому, синглтон.

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

  

Потому что я буду нуждаться в них в самых разных местах в моей игре, и общий доступ будет очень удобен.

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

Просто взглянув на список классов в вашем посте, которые, по вашему мнению, должны быть Singletons, я могу сказать, что половина из них действительно не должна быть одиночными, а другая половина кажется, что они даже не должны быть там в первое место. То, что вам нужно передать объекты вокруг, кажется, связано с отсутствием правильной инкапсуляции, а не с фактическими хорошими вариантами использования для одиночных игроков.

Давайте по-разному рассмотрим ваши классы:


PlayerData (score, lives, ...)
LevelData (parameters per levels and level packs)
GameData (you may obtain some data from server to configure the game)

Просто из имен классов, я бы сказал, что эти классы пахнут как Модель анемичного домена антипаттерн. Анемические объекты обычно приводят к большому количеству данных, которые необходимо передать, что увеличивает сцепление и делает остальную часть кода громоздкой. Кроме того, анемичный класс скрывает тот факт, что вы, вероятно, все еще думаете о процедуре, а не об объектной ориентации, чтобы инкапсулировать детали.


IAP (for in purchases)
Ads (for showing ads)

Почему эти классы должны быть одиночными? Мне кажется, что это должны быть недолговечные классы, которые нужно воспитывать, когда они необходимы и деконструируются, когда пользователь заканчивает покупку или когда объявления больше не нужно показывать.


EntityComponentSystemManager (mananges entity creation and manipulation)

Другими словами, конструктор и сервисный уровень? Почему этот класс не имеет четких границ или цели даже там, в первую очередь?


PlayerProgress (passed levels, stars)

Почему игрок продвигается отдельно от класса Player? Класс Player должен знать, как отслеживать свой прогресс, если вы хотите реализовать отслеживание прогресса в другом классе, чем класс Player для разделения ответственности, то PlayerProgress должен быть за Player.


Box2dManager (manages the physics world)

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


Analytics (for collecting some analytics)
SocialConnection (Facebook and Twitter login, share, friend list, ...)

Это, по-видимому, единственные классы, где Singleton может быть разумным, потому что объекты социальной связи на самом деле не являются вашими объектами. Социальные связи - это всего лишь тень внешних объектовкоторый живет в удаленной службе. Другими словами, это своего рода кеш. Просто убедитесь, что вы четко отделяете часть кэширования от сервисной части внешней службы, потому что последняя часть не должна быть одиночной.

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

ответил Lie Ryan 2 +04002014-10-02T18:16:27+04:00312014bEurope/MoscowThu, 02 Oct 2014 18:16:27 +0400 2014, 18:16:27
-1

Синглтоны для меня ВСЕГДА плохо.

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

Говоря что-то в глобальном масштабе, вы в основном говорите «этот hting - бог и не имеет хозяина» в приведенных выше примерах, я бы сказал, что большинство из них либо являются вспомогательными классами, либо GameServices, и в этом случае игра будет ответственна для них.

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

Поместите его более прямо ... Единственный реальный синглтон - игра, так как она владеет каждой частью кода.

Это ответ основан на субъективном характере вопроса и основан исключительно на моем мнении, он может быть неправильным для всех разработчиков там.

Изменить: по запросу ...

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

Итак, я добавил следующее ...

class Game
{
   IList<GameService> Services;

   public T GetService<T>() where T : GameService
   {
       return Services.FirstOrDefatult(s => s is T);
   }
}

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

public static class Sys
{
    // only the main game assembly can set this (done by the game ctor)
    // anything can get
    public static Game Game { get; internal set; }
}

И использование ...

Из любого места я могу сделать что-то вроде

var tService = Sys.Game.getService<T>();
tService.Something();

Этот подход означает, что моя игра «Собаки», которая обычно считается «одиночной», и я могу расширить базовый класс GameService, но я хочу. У меня есть интерфейсы, такие как IUpdateable, что моя игра проверяет и звонит в любые службы, которые реализуют ее по мере необходимости для конкретных ситуаций.

У меня также есть службы, которые наследуют /расширяют другие службы для более конкретных сценариев сценариев.

Например ...

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

ответил War 30 thEurope/Moscowp30Europe/Moscow09bEurope/MoscowTue, 30 Sep 2014 16:29:08 +0400 2014, 16:29:08
-1

Существенным компонентом элиминации одиночного течения, которого оппоненты синглтона часто не учитывают, является добавление дополнительной логики для решения гораздо более сложного состояния, которое объекты инкапсулируют, когда синглтоны уступают место значениям экземпляра. Действительно, даже определение состояние объекта после замены одноэлементной переменной экземпляра может быть трудным, но программисты, которые заменяют одиночные числа экземплярами переменных, должны быть готовы справиться с ним.

Сравните, например, код кода с помощью .NET HashMap. Оба они очень похожи, но они имеют ключевое различие: Java Dictionary жестко закодирован для использования HashMap и equals (он эффективно использует одноэлементный компаратор), а .NET hashCode может принимать экземпляр Dictionary в качестве параметра конструктора. Стоимость выполнения IEqualityComparer<T> минимальная, но сложность, которую она вводит, не является.

Поскольку каждый экземпляр IEqualityComparer инкапсулирует Dictionary, его состояние - это не просто сопоставление между фактически содержащимися ключами и связанными с ними значениями, а вместо этого отображение между наборы эквивалентности, определяемые ключами и компаратором , и их ассоциированные значения. Если два экземпляра IEqualityComparer и d1 d2 содержат ссылки на один и тот же Dictionary), можно будет создать новый экземпляр IEqualityComparer, который для любого d3, x будет равен d3.ContainsKey(x). Если, однако, d1.ContainsKey(x) || d2.ContainsKey(x) и d1 могут содержать ссылки на разные произвольные сопоставления равенства, вообще не будет способа их слияния, чтобы обеспечить выполнение этого условия.

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

ответил supercat 1 +04002014-10-01T01:57:17+04:00312014bEurope/MoscowWed, 01 Oct 2014 01:57:17 +0400 2014, 01:57:17

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132