Jak mogę uniknąć posiadania wielu singletonów w mojej architekturze gry?

55

Do tworzenia gier używam silnika gry cocos2d-x. Silnik wykorzystuje już wiele singletonów. Jeśli ktoś go użył, powinien znać niektóre z nich:

Director
SimpleAudioEngine
SpriteFrameCache
TextureCache
EventDispatcher (was)
ArmatureDataManager
FileUtils
UserDefault

i wiele innych z ogólną liczbą około 16 klas. Możesz znaleźć podobną listę na tej stronie: Obiekty Singleton w Cocos2d-html5 v3.0 Ale kiedy chcę pisać, potrzebuję znacznie więcej singletonów:

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

Dlaczego uważam, że powinni być singletonami? Ponieważ będę ich potrzebować w bardzo różnych miejscach w mojej grze, a wspólny dostęp byłby bardzo przydatny. Innymi słowy, nie mam gdzieś ich tworzyć i przekazuję wskazówki całej mojej architekturze, ponieważ będzie to bardzo trudne. Są to też rzeczy, których potrzebuję tylko jeden. W każdym razie będę potrzebować kilku, mogę też użyć wzoru Multiton. Ale najgorsze jest to, że Singleton jest najbardziej krytykowanym wzorem z powodu:

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

Można tu znaleźć kilka przemyśleń: https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons i https://stackoverflow.com/questions/4074154/when-should-the-singleton -pattern-nie-być-użyty-poza-oczywiste

Więc myślę, że robię coś złego. Myślę, że mój kod pachnie . :) Zastanawiam się, jak bardziej doświadczeni twórcy gier rozwiązują ten problem architektoniczny? Chcę to sprawdzić, może nadal jest to normalne w rozwoju gier, aby mieć ponad 30 singletonów , uważanych za te, które są już w silniku gry.

Myślałem o użyciu fasady Singleton, która będzie zawierała instancje wszystkich tych klas, których potrzebuję, ale każda z nich nie byłaby już singletonem. To wyeliminuje wiele problemów i miałbym tylko jeden singleton, którym byłaby sama Fasada. Ale w tym przypadku będę miał inny problem projektowy. Fasada stanie się PRZEDMIOTEM BOGA. Myślę, że to też pachnie . Więc nie mogę znaleźć dobrego rozwiązania projektowego dla tej sytuacji. Proszę o radę.

Narek
źródło
26
Większość ludzi argumentujących przeciwko Singletonowi nie zdaje sobie sprawy, że praca wymagana do rozpowszechnienia niektórych danych / funkcji przez cały kod może być bardziej źródłem błędów niż używanie Singletona. Więc w zasadzie nie porównują, po prostu odrzucają ==> podejrzewam, że anty Singletonizm jest postawą :-) No cóż, Singleton ma wady, więc jeśli możesz tego uniknąć, zrób to, aby uniknąć problemów z tym związanych , teraz jeśli nie możesz, sięgnij po to, nie oglądając się za siebie. W końcu Cocos2d wydaje się działać całkiem dobrze z 16 singletonami ...
GameAlchemist
6
Przypominamy, że jest to pytanie o sposób wykonania określonego zadania. To nie jest pytanie o pro / con dyskusję na temat singletonów (które i tak byłyby tutaj nie na temat). Ponadto pamiętaj, że komentarze powinny być wykorzystywane do żądania wyjaśnień od pytającego, a nie jako platforma do stycznej dyskusji na temat zalet i wad singletonów. Jeśli chcesz przekazać swoje uwagi, właściwym sposobem jest udzielenie uzasadnionej odpowiedzi na pytanie , w którym możesz zahartować swoje rozwiązanie poradą za lub przeciw singletonowi.
Josh
Zakładam, że te singletony są wtedy globalnie dostępne? Byłby to jeden z powodów, aby uniknąć tego wzorca.
Peter - Przywróć Monikę

Odpowiedzi:

41

Inicjuję moje usługi w mojej głównej klasie aplikacji, a następnie przekazuję je jako wskaźniki do wszystkich potrzebnych do ich użycia przez konstruktory lub funkcje. Jest to przydatne z dwóch powodów.

Po pierwsze, kolejność inicjalizacji i czyszczenia jest prosta i jasna. Nie ma sposobu, aby przypadkowo zainicjować jedną usługę w innym miejscu, tak jak można to zrobić za pomocą singletona.

Po drugie, jasne jest, kto zależy od czego. Z deklaracji klasy mogę łatwo zobaczyć, jakie klasy potrzebują jakich usług.

Możesz myśleć, że irytujące jest przekazywanie wszystkich tych obiektów, ale jeśli system jest dobrze zaprojektowany, to wcale nie jest źle i sprawia, że ​​wszystko jest znacznie wyraźniejsze (dla mnie i tak).

megadan
źródło
34
+1. Ponadto „irytacja” związana z przekazywaniem odniesień do systemów pomaga przeciwdziałać pełzaniu zależności; jeśli musisz przekazać coś do „ wszystkiego ”, to dobry znak, że masz zły problem ze sprzężeniem w swoim projekcie i jest to o wiele wyraźniejsze w ten sposób niż z globalnym dostępem do wszystkiego zewsząd.
Josh
2
To tak naprawdę nie zapewnia rozwiązania ... więc masz swoją klasę programów z mnóstwem obiektów singletonów, a teraz 10 poziomów w kodzie wymaga czegoś z tych singletonów ... jak to jest przekazywane?
Wojna
Wardy: Dlaczego mieliby być singletonami? Oczywiście, przeważnie możesz omijać określoną instancję, ale to, co sprawia, że ​​singleton jest singletonem, to NIE MOŻESZ użyć innej instancji. Dzięki iniekcji możesz przekazać jedną instancję dla jednego obiektu, a drugą dla następnego. To, że nie robisz tego z jakiegokolwiek powodu, nie oznacza, że ​​nie mogłeś.
Parar
3
Nie są singletonami. Są to zwykłe obiekty, jak wszystko inne, z dobrze zdefiniowanym właścicielem, który kontroluje init / cleanup. Jeśli masz 10 poziomów bez dostępu, być może masz problem z projektem. Nie wszystko musi lub powinno mieć dostęp do rendererów, audio i tym podobnych. W ten sposób myślisz o swoim projekcie. Jako dodatkowy bonus dla tych, którzy testują swój kod (co?), Testowanie jednostkowe staje się znacznie łatwiejsze.
megadan
2
Tak, myślę, że wstrzykiwanie zależności (z lub bez frameworka takiego jak Spring) jest idealnym rozwiązaniem do obsługi tego. Uważam, że jest to świetny artykuł dla tych, którzy zastanawiają się, jak poprawić strukturę kodu, aby uniknąć przekazywania rzeczy wszędzie: misko.hevery.com/2008/10/10/21/...
megadan
17

Nie będę omawiać zła za singletonami, ponieważ Internet może to zrobić lepiej ode mnie.

W moich grach używam wzorca Service Locator, aby uniknąć mnóstwa singletonów / menedżerów.

Pomysł jest dość prosty. Masz tylko jednego Singletona, który działa jak jedyny interfejs, aby dotrzeć do tego, czego używałeś jako Singleton. Zamiast kilku Singletonów masz teraz tylko jeden.

Połączenia takie jak:

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

Wygląda to tak, używając wzorca Service Locator:

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

Jak widać, każdy Singleton jest zawarty w Service Locator.

Aby uzyskać bardziej szczegółowe wyjaśnienie, sugeruję przeczytanie tej strony na temat korzystania z Wzorca Lokalizatora .

[ EDYCJA ]: jak wskazano w komentarzach, strona gameprogrammingpatterns.com zawiera również bardzo interesującą sekcję dotyczącą Singleton .

Mam nadzieję, że to pomoże.

lvictorino
źródło
4
Strona, do której prowadzi link tutaj, gameprogrammingpatterns.com, zawiera również rozdział dotyczący Singletonów, który wydaje się odpowiedni.
ajp15243
28
Lokalizatory usług to po prostu singletony, które przenoszą wszystkie normalne problemy do środowiska wykonawczego, a nie do kompilacji. Jednak to, co sugerujesz, to gigantyczny rejestr statyczny, który jest prawie lepszy, ale nadal jest w zasadzie masą zmiennych globalnych. Jest to po prostu kwestia złej architektury, jeśli wiele poziomów potrzebuje dostępu do tych samych obiektów. Potrzebne jest prawidłowe wstrzyknięcie zależności, nie inaczej nazwane globale niż obecne globały.
Mag
2
Czy potrafisz opracować „właściwy zastrzyk zależności”?
Blue Wizard
17
To naprawdę nie pomaga w rozwiązaniu problemu. Zasadniczo jest to wiele singletonów połączonych w jeden gigantyczny singleton. Żaden z podstawowych problemów strukturalnych nie został naprawiony ani nawet rozwiązany, kod nadal śmierdzi teraz tylko ładniejszy.
ClassicThunder
4
@classicthunder Chociaż nie rozwiązuje to każdego problemu, jest to duża poprawa w stosunku do Singletonów, ponieważ rozwiązuje kilka. W szczególności kod, który piszesz, nie jest już połączony z jedną klasą, ale raczej interfejsem / kategorią klas. Teraz możesz łatwo zamieniać różne implementacje różnych usług.
jhocking
12

Czytając wszystkie te odpowiedzi, komentarze i artykuły wskazane, szczególnie te dwa genialne artykuły,

w końcu doszedłem do następującego wniosku, który jest rodzajem odpowiedzi na moje własne pytanie. Najlepszym podejściem jest nie być leniwym i przekazywać zależności bezpośrednio. Jest to wyraźne, testowalne, nie ma globalnego dostępu, może wskazywać zły projekt, jeśli musisz przekazać delegatów bardzo głęboko i w wielu miejscach i tak dalej. Ale w programowaniu jeden rozmiar nie pasuje do wszystkich. Dlatego nadal istnieją przypadki, w których przekazywanie ich bezpośrednio nie jest przydatne. Na przykład w przypadku, gdy stworzymy środowisko gry (szablon kodu, który zostanie ponownie użyty jako kod podstawowy do opracowywania różnych rodzajów gier) i uwzględnimy tam wiele usług. Tutaj nie wiemy, jak głęboko można przejść każdą usługę i ile miejsc będzie to konieczne. To zależy od konkretnej gry. Co więc robimy w tego rodzaju przypadkach? Używamy wzorca projektowego Service Locator zamiast Singleton,

  1. Nie programujesz do implementacji, ale do interfejsów. Jest to bardzo istotne z punktu widzenia zasad OOP. W czasie wykonywania można zmienić lub nawet wyłączyć usługę za pomocą wzorca Null Object. Jest to ważne nie tylko ze względu na elastyczność, ale bezpośrednio pomaga w testowaniu. Możesz wyłączyć, wyśmiewać, a także użyć wzoru dekoratora, aby dodać trochę rejestrowania i inne funkcje. To bardzo ważna właściwość, której Singleton nie ma.
  2. Kolejną ogromną zaletą jest to, że możesz kontrolować żywotność obiektu. W Singleton kontrolujesz tylko czas, w którym obiekt zostanie spawnowany według leniwego wzoru inicjalizacji. Ale gdy obiekt zostanie spawnowany, będzie żył do końca programu. Ale w niektórych przypadkach chcesz zmienić usługę. Lub nawet zamknij to. Ta elastyczność jest bardzo ważna, zwłaszcza w grach.
  3. Łączy / zawija kilka Singletonów w jednym kompaktowym API. Myślę, że możesz również użyć niektórych Lokalizatorów usług fasadowych, które dla jednej operacji mogą korzystać z kilku usług jednocześnie.
  4. Możesz argumentować, że nadal mamy globalny dostęp, który jest głównym problemem projektowym w Singleton. Zgadzam się, ale będziemy potrzebować tego wzorca i tylko wtedy, gdy będziemy potrzebować globalnego dostępu. Nie powinniśmy narzekać na globalny dostęp, jeśli uważamy, że bezpośredni zastrzyk zależności nie jest przydatny w naszej sytuacji i potrzebujemy globalnego dostępu. Ale nawet w przypadku tego problemu dostępna jest poprawa (patrz drugi artykuł powyżej). Możesz ustawić wszystkie usługi jako prywatne i odziedziczyć po klasie Service Locator wszystkie klasy, które według nich powinny korzystać z usług. Może to znacznie ograniczyć dostęp. I proszę wziąć pod uwagę, że w przypadku Singletona nie można używać dziedziczenia z powodu prywatnego konstruktora.

Powinniśmy również wziąć pod uwagę, że ten wzorzec był używany w Unity, LibGDX, XNA. Nie jest to zaletą, ale jest to rodzaj dowodu użyteczności wzoru. Pomyśl, że silniki te zostały opracowane przez wielu inteligentnych programistów przez długi czas, a podczas faz ewolucji silnika wciąż nie znaleźli lepszego rozwiązania.

Podsumowując, myślę, że Service Locator może być bardzo przydatny w scenariuszach opisanych w moim pytaniu, ale nadal należy go unikać, jeśli jest to możliwe. Jako ogólna zasada - użyj go, jeśli musisz.

EDYCJA: Po roku pracy i korzystaniu z bezpośredniej DI oraz problemach z dużymi konstruktorami i wieloma delegacjami przeprowadziłem więcej badań i znalazłem dobry artykuł, który mówi o zaletach i wadach metod DI i Lokalizatora usług. Powołując się na ten artykuł Martina Fowlera ( http://www.martinfowler.com/articles/injection.html ), który wyjaśnia, kiedy lokalizatory usług są złe:

Zatem głównym problemem są osoby, które piszą kod, który spodziewa się, że zostanie użyty w aplikacjach poza kontrolą programu piszącego. W takich przypadkach problem stanowi nawet minimalne założenie dotyczące Lokalizatora usług.

Ale w każdym razie, jeśli chcesz nam DI po raz pierwszy, powinieneś przeczytać ten http://misko.hevery.com/2008/10/21/dependency-injection-myth-reference-passing/ artykuł (wskazany przez @megadan) , zwracając szczególną uwagę na prawo Demetera (LoD) lub zasadę najmniejszej wiedzy .

Narek
źródło
Moja osobista nietolerancja wobec lokalizatorów usług wynika głównie z moich prób testowania kodu, który ich używa. W interesie testowania czarnej skrzynki wywołujesz konstruktora, aby utworzyć instancję do przetestowania. Wyjątek może zostać zgłoszony natychmiast lub przy dowolnym wywołaniu metody, a właściwości publiczne mogą prowadzić do brakujących zależności. Może to być niewielkie, jeśli masz dostęp do źródła, ale nadal narusza ono zasadę najmniejszej niespodzianki (częsty problem z Singletonami). Zasugerowałeś podanie lokalizatora usług, który łagodzi część tego, i należy za to pochwalić.
Mag
3

Często zdarza się, że części kodu są uważane za obiekty fundamentalne lub klasę podstawową, ale nie uzasadnia to, aby jego cykl życia był dyktowany jako Singleton.

Programiści często polegają na wzorze Singleton jako na wygodzie i czystym lenistwie, zamiast przyjmować alternatywne podejście i być nieco bardziej gadatliwym i narzucającym relacje między obiektami w wyraźnej posiadłości.

Zadaj sobie więc pytanie, które łatwiej utrzymać.

Jeśli jesteś podobny do mnie, wolę być w stanie otworzyć plik nagłówka i krótko przejrzeć argumenty konstruktora i metody publiczne, aby zobaczyć, jakich zależności wymaga obiekt, niż sprawdzać setki wierszy kodu w pliku implementacji.

Jeśli uczyniłeś obiekt Singletonem, a później zdasz sobie sprawę, że musisz być w stanie obsługiwać wiele instancji tego obiektu, wyobraź sobie, jak wielkie zmiany są potrzebne, jeśli ta klasa była czymś, z czego korzystałeś dość często w swojej bazie kodu. Lepszą alternatywą jest wyraźne określenie zależności, nawet jeśli obiekt jest przydzielany tylko raz, a jeśli zajdzie potrzeba obsługi wielu instancji, można to zrobić bezpiecznie bez obaw.

Jak zauważyli inni, użycie wzoru Singleton zaciemnia również sprzężenie, które często oznacza zły projekt i wysoką spójność między modułami. Taka spójność jest zwykle niepożądana i prowadzi do kruchego kodu, który może być trudny do utrzymania.

Naros
źródło
-1: Ta odpowiedź błędnie opisuje wzorzec singletonu, a wszystkie jego argumenty opisują problemy ze słabą implementacją wzorca. Właściwy singleton jest interfejsem i pozwala uniknąć każdego opisanego problemu (w tym przejścia na brak pojedynczości, który jest scenariuszem, który GoF wyraźnie rozwiązuje). Jeśli trudno jest zrefaktoryzować użycie singletonu, wówczas refaktoryzacja użycia wyraźnego odwołania do obiektu może być tylko trudniejsza, a nie mniej. Jeśli singleton w jakiś sposób powoduje WIĘCEJ kodu, po prostu zostało zrobione źle.
Seth Battin
1

Singleton jest znanym wzorcem, ale dobrze jest znać cele, które służy, a także zalety i wady.

To naprawdę ma sens, jeśli w ogóle nie ma związku .
Jeśli potrafisz obsłużyć swój komponent przy pomocy zupełnie innego obiektu (bez silnej zależności) i oczekujesz tego samego zachowania, singleton może być dobrym wyborem.

Z drugiej strony, jeśli potrzebujesz niewielkiej funkcjonalności , używanej tylko w określonych porach, powinieneś preferować przekazywanie referencji .

Jak wspomniałeś, singletony nie mają kontroli przez całe życie. Ale zaletą jest to, że mają leniwą inicjalizację.
Jeśli nie nazwiesz tego singletonu za życia aplikacji, nie zostanie on utworzony. Ale wątpię, żebyś budował singletony i ich nie używał.

Musisz zobaczyć singleton jak usługę zamiast komponentu .

Singletony działają, nigdy nie złamały żadnej aplikacji. Ale ich braki (cztery podane przez ciebie) to powody, dla których nie zrobisz żadnego.

Crazyrems
źródło
1

Silnik wykorzystuje już wiele singletonów. Jeśli ktoś go użył, powinien znać niektóre z nich:

Nieznajomość nie jest powodem, dla którego należy unikać singletonów.

Istnieje wiele dobrych powodów, dla których Singleton jest konieczny lub nieunikniony. Ramy gier często używają singletonów, ponieważ jest to nieunikniona konsekwencja posiadania tylko jednego stanowego sprzętu. Nie ma sensu nigdy kontrolować tych programów za pomocą wielu instancji odpowiednich programów obsługi. Powierzchnia graficzna jest zewnętrznym stanowym sprzętem i przypadkowe zainicjowanie drugiej kopii podsystemu graficznego jest katastrofalne, ponieważ teraz oba podsystemy graficzne będą ze sobą walczyć o to, kto będzie rysował i kiedy, nadpisując się w niekontrolowany sposób. Podobnie z systemem kolejek zdarzeń będą walczyć o to, kto otrzyma zdarzenia myszy w nieokreślony sposób. W przypadku stanowego sprzętu zewnętrznego, w którym jest tylko jeden, singleton jest nieunikniony, aby zapobiec konfliktom.

Innym miejscem, w którym Singleton jest rozsądny, są menedżerowie Cache. Skrytki są szczególnym przypadkiem. Nie należy ich tak naprawdę uważać za singletonów, nawet jeśli używają tych samych technik co singletony, aby pozostać przy życiu i żyć wiecznie. Usługi buforowania są usługą przezroczystą, nie powinny zmieniać zachowania programu, więc jeśli zastąpisz usługę pamięci podręcznej zerową pamięcią podręczną, program powinien nadal działać, z wyjątkiem tego, że działa tylko wolniej. Głównym powodem, dla którego menedżerowie pamięci podręcznej są wyjątkiem od singletonu, jest to, że nie ma sensu zamykać usługi pamięci podręcznej przed zamknięciem samej aplikacji, ponieważ spowoduje to również odrzucenie buforowanych obiektów, co nie pozwala na to, aby była ona singel.

Są to dobre powody, dla których usługi te są singletonami. Jednak żaden z dobrych powodów, aby mieć singletony, nie dotyczy wymienionych klas.

Ponieważ będę ich potrzebować w bardzo różnych miejscach w mojej grze, a wspólny dostęp byłby bardzo przydatny.

To nie są powody do singletonów. To są powody globalizacji. Jest to również oznaką złego projektu, jeśli musisz przekazać wiele rzeczy wokół różnych systemów. Konieczność przekazywania rzeczy wskazuje na wysokie sprzężenie, któremu można zapobiec dzięki dobrej konstrukcji OO.

Wystarczy spojrzeć na listę klas w twoim poście, które Twoim zdaniem powinny być singletonami, mogę powiedzieć, że połowa z nich tak naprawdę nie powinna być singletonami, a druga połowa wydaje się, że nie powinny tam być. To, że musisz przekazywać obiekty, wydaje się wynikać z braku właściwej enkapsulacji, a nie z rzeczywistych dobrych przypadków użycia singletonów.

Spójrzmy na twoje klasy krok po kroku:


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

Po prostu z nazw klas powiedziałbym, że klasy te pachną jak antymateriał Anemic Domain Model . Obiekty anemiczne zwykle powodują przekazanie dużej ilości danych, co zwiększa sprzężenie i sprawia, że ​​reszta kodu jest niewygodna. Ponadto klasa anemiczna ukrywa fakt, że prawdopodobnie nadal myślisz proceduralnie, zamiast używać orientacji obiektowej do kapsułkowania szczegółów.


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

Dlaczego te klasy muszą być singletonami? Wydaje mi się, że powinny to być zajęcia krótkotrwałe, które należy wychowywać, gdy są potrzebne, i dekonstruować je, gdy użytkownik zakończy zakup lub gdy reklamy nie będą już wyświetlane.


EntityComponentSystemManager (mananges entity creation and manipulation)

Innymi słowy, konstruktor i warstwa usługi? Dlaczego w ogóle ta klasa nie ma wyraźnych granic ani celu?


PlayerProgress (passed levels, stars)

Dlaczego postęp gracza jest oddzielony od klasy gracza? Klasa Player powinna wiedzieć, jak śledzić swoje postępy, jeśli chcesz wdrożyć śledzenie postępu w innej klasie niż klasa Player w celu rozdzielenia odpowiedzialności, to PlayerProgress powinien stać za Graczem.


Box2dManager (manages the physics world)

Naprawdę nie mogę komentować tego dalej, nie wiedząc, co właściwie robi ta klasa, ale jedno jest jasne, że ta klasa jest źle nazwana.


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

Wydaje się, że są to jedyne klasy, w których Singleton może być uzasadniony, ponieważ obiekty połączenia społecznego nie są w rzeczywistości obiektami. Połączenia społecznościowe to tylko cień zewnętrznych obiektów, które żyją w zdalnej usłudze. Innymi słowy, jest to rodzaj pamięci podręcznej. Upewnij się tylko, że wyraźnie oddzieliłeś część buforującą od części serwisowej usługi zewnętrznej, ponieważ ta ostatnia część nie powinna być singletonem.

Jednym ze sposobów uniknięcia przekazywania instancji tych klas jest użycie przekazywania wiadomości. Zamiast nawiązywać bezpośrednie połączenia z instancjami tych klas, zamiast tego wysyłasz wiadomość skierowaną do usługi analitycznej i usługi SocialConnection asynchronicznie, a usługi te są subskrybowane, aby otrzymywać te wiadomości i działać na podstawie wiadomości. Ponieważ kolejka zdarzeń jest już singletonem, trampolując połączenie, unikniesz konieczności przekazywania rzeczywistej instancji podczas komunikacji z singletonem.

Lie Ryan
źródło
-1

Singletony są dla mnie ZAWSZE złe.

W mojej architekturze stworzyłem kolekcję GameServices, którą zarządza klasa gier. W razie potrzeby mogę wyszukać dodać i usunąć z tej kolekcji.

Mówiąc, że coś ma zasięg globalny, w zasadzie mówisz „to uderzenie jest bogiem i nie ma mistrza” w przytoczonych powyżej przykładach powiedziałbym, że większość z nich to klasy pomocnicze lub GameServices, w którym to przypadku gra byłaby za nie odpowiedzialna.

Ale to tylko mój projekt w moim silniku, Twoja sytuacja może być inna.

Mówiąc bardziej bezpośrednio ... Jedynym prawdziwym singletonem jest gra, ponieważ posiada każdą część kodu.

Ta odpowiedź jest oparta na subiektywnym charakterze pytania i opiera się wyłącznie na mojej opinii, może nie być poprawna dla wszystkich programistów.

Edycja: zgodnie z żądaniem ...

Ok, to jest z mojej głowy, ponieważ w tej chwili nie mam przed sobą mojego kodu, ale w gruncie rzeczy u podstaw każdej gry leży klasa gier, która jest naprawdę singletonem ... tak musi być!

Więc dodałem następujące ...

class Game
{
   IList<GameService> Services;

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

Teraz mam także klasę systemową (statyczną), która zawiera wszystkie moje singletony z całego programu (zawiera bardzo niewiele opcji gry i konfiguracji i tyle o tym) ...

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; }
}

I użycie ...

Z dowolnego miejsca mogę zrobić coś takiego

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

Takie podejście oznacza, że ​​moja gra „posiada” rzeczy, które zwykle byłyby uważane za „singleton” i mogę rozszerzyć podstawową klasę GameService, jak chcę. Mam interfejsy takie jak IUpdateable, dzięki którym moja gra sprawdza i wywołuje wszelkie usługi, które implementują go w razie potrzeby w określonych sytuacjach.

Mam również usługi, które dziedziczą / rozszerzają inne usługi dla bardziej szczegółowych scenariuszy.

Na przykład ...

Mam usługę fizyki, która obsługuje moje symulacje fizyki, ale w zależności od sceny symulacja fizyki może wymagać nieco innych zachowań.

Wojna
źródło
1
Czy usługi gier nie powinny być tworzone tylko raz? Jeśli tak, to tylko wzorzec singletonu, który nie jest egzekwowany na poziomie klasy, co jest gorsze niż poprawnie wdrożony singleton.
GameAlchemist
2
@Wardy Nie rozumiem jasno, co zrobiłeś, ale to brzmi jak opisana przeze mnie Singleton-Fasada i powinna stać się przedmiotem BOGA, prawda?
Narek
10
To tylko wzór Singleton zaimplementowany na poziomie gry. Zasadniczo więc najciekawszym aspektem jest udawanie, że nie używasz Singletona. Przydatne, jeśli twój szef pochodzi z kościoła anty-wzorcowego.
GameAlchemist
2
Nie jestem pewien, jak to skutecznie różni się od używania Singletonów. Czy nie jest jedyna różnica w tym, jak konkretnie uzyskujesz dostęp do swojej istniejącej usługi tego typu? Tj. var tService = Sys.Game.getService<T>(); tService.Something();Zamiast pomijać getServiceczęść i uzyskiwać do niej bezpośredni dostęp?
Christian
1
@Christian: Rzeczywiście, właśnie to jest antipattern Service Locator. Najwyraźniej ta społeczność nadal uważa, że ​​jest to zalecany wzorzec projektowy.
Mag
-1

Istotnym składnikiem eliminacji singletonów, którego przeciwnicy singletonu często nie biorą pod uwagę, jest dodanie dodatkowej logiki do radzenia sobie ze znacznie bardziej skomplikowanym stanem, w którym obiekty zamykają się, gdy singletony ustępują wartościom instancji. Rzeczywiście, nawet zdefiniowanie stanu obiektu po zastąpieniu singletonu zmienną instancji może być trudne, ale programiści, którzy zastępują singletony zmiennymi instancji, powinni być przygotowani, aby sobie z tym poradzić.

Porównaj na przykład Java HashMapz .NET Dictionary. Oba są dość podobne, ale mają kluczową różnicę: Java HashMapjest mocno zakodowana w użyciu equalsi hashCode(efektywnie wykorzystuje komparator singletonów), podczas gdy .NET Dictionarymoże zaakceptować instancję IEqualityComparer<T>jako parametr konstruktora. Koszt utrzymania tego okresu IEqualityComparerjest minimalny, ale złożoność, którą wprowadza, nie jest.

Ponieważ każda Dictionaryinstancja zawiera enkapsulację IEqualityComparer, jej stan nie jest jedynie mapowaniem między kluczami, które faktycznie zawiera, a ich powiązanymi wartościami, ale zamiast tego mapowaniem między zestawami równoważności zdefiniowanymi przez klucze i komparatorem i ich powiązanymi wartościami. Jeśli dwie instancje d1i d2z Dictionaryodniesieniami trzymają się tak samo IEqualityComparer, to będzie można produkować nową instancję d3taki, że dla każdego x, d3.ContainsKey(x)będzie równy d1.ContainsKey(x) || d2.ContainsKey(x). Jeśli jednak d1i d2mogą zawierać odniesienia do różnych arbitralnych podmiotów porównujących równość, zasadniczo nie będzie sposobu na połączenie ich w celu zapewnienia, że ​​spełniony zostanie warunek.

Dodanie zmiennych instancji w celu wyeliminowania singletonów, ale niepoprawne uwzględnienie możliwych nowych stanów, które mogą być implikowane przez te zmienne instancji, może stworzyć kod, który ostatecznie będzie bardziej kruchy niż w przypadku singletonu. Jeśli każda instancja obiektu ma osobne pole, ale wszystko źle funkcjonuje, chyba że wszystkie zidentyfikują tę samą instancję obiektu, wówczas wszystkie nowe kupione pola to zwiększona liczba możliwych błędów.

supercat
źródło
1
Chociaż jest to zdecydowanie interesujący aspekt dyskusji, nie sądzę, aby faktycznie dotyczyła pytania postawionego przez PO.
Josh
1
@JoshPetrie: Komentarze do oryginalnego postu (np. Magusa) sugerowały, że koszt czasu wykonania przeniesienia referencji w celu uniknięcia korzystania z singletonów nie był świetny. W związku z tym uważam, że istotne są koszty semantyczne związane z tym, że każdy obiekt zawiera odniesienia do celu unikania singletonów. Należy unikać singletonów w przypadkach, w których można ich łatwo i tanio uniknąć, ale kod, który nie wymaga jawnie singletonu, ale może ulec awarii, gdy tylko wystąpi wiele wystąpień czegoś, jest gorszy niż kod używający singletonu.
supercat
2
Odpowiedzi nie służą do adresowania komentarzy, lecz do bezpośredniej odpowiedzi na pytanie.
ClassicThunder
@ClassicThunder: Czy podoba Ci się ta edycja?
supercat