Alternatywy dla singletonów / globałów

16

Słyszałem niezliczoną ilość razy o pułapkach Singletonów / globali i rozumiem, dlaczego tak często się im nie podobają.

Nie rozumiem, czym jest elegancka, nieuporządkowana alternatywa. Wydaje się, że alternatywa dla korzystania z singletonów / globałów zawsze polega na przepuszczaniu obiektów o milion poziomów w dół przez obiekty silnika, aż dotrą one do obiektów, które ich potrzebują.

Na przykład w mojej grze wstępnie ładuję niektóre zasoby, gdy gra się uruchamia. Zasoby te są wykorzystywane dopiero później, gdy gracz porusza się po menu głównym i wchodzi do gry. Czy mam przekazać te dane z mojego obiektu gry do mojego obiektu ScreenManager (pomimo faktu, że tylko jeden ekran faktycznie dba o te dane), a następnie do odpowiedniego obiektu ekranu i gdziekolwiek indziej?

Wydaje się po prostu, że handluję danymi o stanie globalnym w celu wstrzykiwania zaśmieconej zależności, przekazując dane do obiektów, które nawet nie dbają o dane, z wyjątkiem celu przekazania ich do obiektów potomnych.

Czy to przypadek, w którym Singleton byłby dobrą rzeczą, czy jest jakieś eleganckie rozwiązanie, którego mi brakuje?

wargoński
źródło

Odpowiedzi:

16

Nie łącz singletonów i globali. Chociaż zwykle potrzebne są pewne zmienne globalne, singleton nie jest tylko zamiennikiem zmiennej globalnej, ale przede wszystkim sposobem obejścia problemów ze statyczną kolejnością inicjalizacji w C ++ ( i FQA ). (W innych językach jest to sposób na obejście różnych braków językowych, takich jak brak zmiennych globalnych i podstawowych funkcji).

Jeśli możesz po prostu użyć globalnego wskaźnika zamiast singletonu i upewnić się, że jest on zainicjowany (ręcznie), zanim cokolwiek będzie go potrzebować, unikniesz wywołania funkcji i narzutu gałęzi, lame składni, aby dostać się do obiektu, i możesz faktycznie zrobić drugie wystąpienie klasy, gdy potrzebujesz do testów lub ponieważ twój projekt się zmienił.

Dla kilku zmiennych globalnych, które chcesz (typowe przykłady to wyjście audio, lista otwartych okien, obsługa klawiatury itp.), Zalecam wzorzec lokalizatora usług . Ułatwia zastępowanie rzeczy różnymi implementacjami (np. Urządzenie audio rzeczywiste i zerowe) i gromadzi wszystkie globale w jedną strukturę, aby uniknąć zanieczyszczenia przestrzeni nazw.


źródło
+1. Dobra odpowiedź i dzięki za link do wzorca lokalizatora usług. To bardzo interesująca lektura.
bummzack
1

Jeśli nie masz / nie możesz mieć części kodu w magiczny sposób „wiedzący” o niektórych danych, to trzeba będzie jakoś przekazać. Nie oznacza to jednak, że należy koniecznie przekazywać tylko argumenty.

W twoim przykładzie, czy nie mógłbyś mieć jakiegoś „AssetManagera”, który ładowałby i przechowywał zasoby, a wtedy ScreenManager musiałby tylko mieć odniesienie do tego (prawdopodobnie podczas tworzenia)? W tym sensie przekazujesz referencje do zasobów opakowanych w inny obiekt i możesz to przekazać raz podczas inicjalizacji, zamiast przekazywać to do funkcji liścia, gdy jest to wymagane.

Teraz IMHO, że AssetManager, będący czymś, czego chcesz tylko jeden, może równie dobrze być singletonem. Pod warunkiem, że rozumiesz pułapki i kod, aby ich uniknąć (załóż, że singleton będzie dostępny jednocześnie z wielu wątków i dźgnij się widelcem za każdym razem, gdy zrobisz coś, co musi zostać zablokowane), a następnie powal się.

JasonD
źródło
1

Myślę, że Jason D ma absolutną rację - tak bym sobie z tym poradził:

Gra ma instancję AssetManager, obiekt, z którego można uzyskać dowolny zasób według nazwy.

W grze:

assetManager = new AssetManager();
screenManager = new ScreenManager();
screenManager.assetManager = assetManager;

W ScreenManager:

screen = new Screen();
screen.assetManager = assetManager;

Na ekranie:

myAsset = assetManager.getBitmp("lava.png");

Teraz wszystkie ekrany mają dostęp do potrzebnych zasobów. Nie jest to nic bardziej złożonego ani szalonego niż używanie globali lub singletonów, a masz możliwość uruchomienia 2 instancji Gry w tej samej aplikacji bez kolizji. Kiedyś musiałem stworzyć grę składającą się z 8 mini-gier, wszystkie dzielące te same klasy podstawowe / framework. Musiałem zmienić wszystkie moje globale / singletony, aby użyć tego stylu przekazywania referencji, i nigdy nie oglądałem się za siebie. Jedynymi rzeczami, które powinny być globalne, są rzeczy, które mogą fizycznie istnieć tylko raz, takie jak audio, sieci, we / wy itp.

Iain
źródło
0

Możesz użyć wzorca fabrycznego, aby zastąpić Singletona . Następnie klasa fabryczna ma kontrolę nad tym, ile wystąpień możesz utworzyć, które możesz łatwo zmienić później, gdy okaże się, że potrzebujesz więcej niż jednego AssetManager. Jak stwierdzono w tym artykule :

Daje to pełną elastyczność Singleton, bez żadnych problemów.


Inną, raczej ograniczoną, możliwością jest uczynienie klasy statyczną (co nie uważam za wykonalne dla AssetManager i możliwe tylko w językach, które w ogóle mają klasy statyczne). Ale działa to tylko wtedy, gdy nie potrzebujesz dziedziczenia / polimorfizmu. To bardzo nieelastyczne rozwiązanie:

metody statyczne są tak elastyczne jak granit. Za każdym razem, gdy używasz jednego, rzucasz konkretną część swojego programu. Tylko upewnij się, że nie zacięła się tam stopa, gdy patrzysz, jak twardnieje. Pewnego dnia będziesz zaskoczony, że, do cholery, naprawdę potrzebujesz kolejnej implementacji tej cholernej klasy PrintSpooler i powinien to być interfejs, fabryka i zestaw klas implementacji. Nie!

Chodzi o metody statyczne, ale można je również zastosować do klas statycznych.

Michael Klement
źródło
Co rozumiesz przez „uczynienie klasy statyczną”? C ++ nie ma klas statycznych. Masz na myśli tylko metody statyczne? Po co więc zawracać sobie głowę klasą zamiast przestrzeni nazw?
1
@Joe: Cóż, pytanie nie koncentruje się na C ++, tak jak je rozumiem. W języku C # lub Java można tworzyć klasy statyczne i mam na myśli te. Ponadto, jak już powiedziałem, klasy statyczne nie są optymalnym rozwiązaniem przez większość czasu, ale w rzadkich przypadkach może działać jak globalny.
Michael Klement