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ę.
źródło
Odpowiedzi:
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).
źródło
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:
Wygląda to tak, używając wzorca Service Locator:
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.
źródło
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,
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:
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 .
źródło
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.
źródło
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.
źródło
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.
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:
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.
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.
Innymi słowy, konstruktor i warstwa usługi? Dlaczego w ogóle ta klasa nie ma wyraźnych granic ani celu?
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.
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.
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.
źródło
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 ...
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) ...
I użycie ...
Z dowolnego miejsca mogę zrobić coś takiego
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ń.
źródło
var tService = Sys.Game.getService<T>(); tService.Something();
Zamiast pomijaćgetService
część i uzyskiwać do niej bezpośredni dostęp?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
HashMap
z .NETDictionary
. Oba są dość podobne, ale mają kluczową różnicę: JavaHashMap
jest mocno zakodowana w użyciuequals
ihashCode
(efektywnie wykorzystuje komparator singletonów), podczas gdy .NETDictionary
może zaakceptować instancjęIEqualityComparer<T>
jako parametr konstruktora. Koszt utrzymania tego okresuIEqualityComparer
jest minimalny, ale złożoność, którą wprowadza, nie jest.Ponieważ każda
Dictionary
instancja 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 instancjed1
id2
zDictionary
odniesieniami trzymają się tak samoIEqualityComparer
, to będzie można produkować nową instancjęd3
taki, że dla każdegox
,d3.ContainsKey(x)
będzie równyd1.ContainsKey(x) || d2.ContainsKey(x)
. Jeśli jednakd1
id2
mogą 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.
źródło