Ich benutze die cocos2d-x Game Engine zum Erstellen von Spielen. Der Motor verbraucht bereits viele Singletons. Wenn jemand es benutzt hat, sollte er mit einigen von ihnen vertraut sein:
Director
SimpleAudioEngine
SpriteFrameCache
TextureCache
EventDispatcher (was)
ArmatureDataManager
FileUtils
UserDefault
und vieles mehr mit insgesamt ca. 16 Klassen. Eine ähnliche Liste finden Sie auf dieser Seite: Singleton-Objekte in Cocos2d-html5 v3.0 Aber wenn ich ein Spiel schreiben will, brauche ich viel mehr Singletons:
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)
.....
Warum denke ich, dass sie Singletons sein sollten? Weil ich sie an sehr unterschiedlichen Orten in meinem Spiel brauche und ein gemeinsamer Zugriff sehr praktisch wäre. Mit anderen Worten, ich weiß nicht, was ich tun soll, um sie irgendwo zu erstellen, und gebe Zeiger an meine gesamte Architektur weiter, da dies sehr schwierig sein wird. Auch das sind Dinge, die ich nur einmal brauche. In jedem Fall brauche ich mehrere, ich kann auch ein Multiton-Muster verwenden. Das Schlimmste ist jedoch, dass Singleton das am häufigsten kritisierte Muster ist, weil:
- bad testability
- no inheritance available
- no lifetime control
- no explicit dependency chain
- global access (the same as global variables, actually)
- ....
Hier finden Sie einige Gedanken: https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons und https://stackoverflow.com/questions/4074154/when-should-the-singleton -muster-nicht-verwendet-werden-neben-dem-offensichtlichen
Also, ich denke, ich mache etwas falsch. Ich denke, mein Code riecht . :) Ich wundere mich, wie mehr erfahrene Spieleentwickler dieses Architekturproblem lösen? Ich möchte überprüfen, ob es in der Spieleentwicklung noch normal ist, mehr als 30 Singletons zu haben, die bereits in der Spiele-Engine enthalten sind.
Ich habe mir überlegt, eine Singleton-Fassade zu verwenden, die Instanzen all dieser Klassen enthält, die ich benötige, aber nicht jede von ihnen wäre bereits ein Singleton. Dadurch werden viele Probleme beseitigt, und ich hätte nur einen einzigen Singleton, der die Fassade selbst wäre. Aber in diesem Fall werde ich ein anderes Designproblem haben. Die Fassade wird zu einem GOTTESOBJEKT. Ich denke, das riecht auch . Daher kann ich für diese Situation keine gute Designlösung finden. Bitte um Rat.
quelle
Antworten:
Ich initialisiere meine Services in meiner Hauptanwendungsklasse und übergebe sie dann als Zeiger auf die Anforderungen, die zur Verwendung durch die Konstruktoren oder Funktionen erforderlich sind. Dies ist aus zwei Gründen nützlich.
Erstens ist die Reihenfolge der Initialisierung und Bereinigung einfach und klar. Es gibt keine Möglichkeit, einen Dienst versehentlich an einem anderen Ort zu initialisieren, wie dies mit einem Singleton möglich ist.
Zweitens ist klar, wer von was abhängt. Ich kann einfach anhand der Klassendeklaration erkennen, welche Klassen welche Dienste benötigen.
Sie mögen denken, es ist ärgerlich, all diese Objekte herumzugeben, aber wenn das System gut entworfen ist, ist es überhaupt nicht schlecht und macht die Dinge viel klarer (für mich jedenfalls).
quelle
Ich werde nicht über das Böse hinter Singletons diskutieren, weil das Internet das besser kann als ich.
In meinen Spielen verwende ich das Service Locator-Muster , um Tonnen von Singletons / Managern zu vermeiden.
Das Konzept ist ziemlich einfach. Sie haben nur einen Singleton, der als einzige Schnittstelle fungiert, um das zu erreichen, was Sie als Singleton verwendet haben. Anstatt mehrere Singleton zu haben, haben Sie jetzt nur noch einen.
Anrufe wie:
So sieht es mit dem Service Locator-Muster aus:
Wie Sie sehen, ist jeder Singleton im Service Locator enthalten.
Um eine detailliertere Erklärung zu erhalten, empfehle ich Ihnen, diese Seite über die Verwendung des Locator-Musters zu lesen .
[ BEARBEITEN ]: Wie in den Kommentaren erwähnt, enthält die Site gameprogrammingpatterns.com auch einen sehr interessanten Abschnitt über Singleton .
Ich hoffe, es hilft.
quelle
Beim Lesen all dieser Antworten, Kommentare und Artikel wurde darauf hingewiesen, insbesondere auf diese beiden brillanten Artikel.
Schließlich bin ich zu folgendem Schluss gekommen, was eine Art Antwort auf meine eigene Frage ist. Der beste Ansatz ist, nicht faul zu sein und Abhängigkeiten direkt zu übergeben. Dies ist explizit, überprüfbar, es gibt keinen globalen Zugriff, kann auf ein falsches Design hinweisen, wenn Sie die Delegierten sehr tief und an vielen Stellen übergeben müssen und so weiter. Aber beim Programmieren passt eine Größe nicht für alle. Daher gibt es immer noch Fälle, in denen es nicht praktisch ist, sie direkt zu übergeben. Zum Beispiel für den Fall, dass wir ein Spiel-Framework erstellen (eine Codevorlage, die als Basiscode für die Entwicklung verschiedener Arten von Spielen wiederverwendet wird) und dort viele Dienste enthalten. Hier wissen wir nicht, wie tief jeder Service übergeben werden kann und an wie vielen Stellen er benötigt wird. Es hängt von einem bestimmten Spiel ab. Was machen wir also in solchen Fällen? Wir verwenden das Service Locator-Entwurfsmuster anstelle von Singleton.
Wir sollten auch berücksichtigen, dass dieses Muster in Unity, LibGDX, XNA verwendet wurde. Dies ist kein Vorteil, aber dies ist eine Art Beweis für die Verwendbarkeit des Musters. Bedenken Sie, dass diese Motoren lange Zeit von vielen intelligenten Entwicklern entwickelt wurden und in den Entwicklungsphasen des Motors noch keine bessere Lösung gefunden haben.
Abschließend denke ich, dass Service Locator für die in meiner Frage beschriebenen Szenarien sehr nützlich sein kann, aber dennoch vermieden werden sollte, wenn es möglich ist. Als Faustregel - verwenden Sie es, wenn Sie müssen.
EDIT: Nach einem Jahr der Arbeit mit Direct DI und Problemen mit riesigen Konstruktoren und vielen Delegationen habe ich mehr Nachforschungen angestellt und einen guten Artikel gefunden, der über Vor- und Nachteile von DI- und Service Locator-Methoden spricht. Zitiert wird dieser Artikel ( http://www.martinfowler.com/articles/injection.html ) von Martin Fowler, der erklärt, wann die Service-Locators tatsächlich schlecht sind:
Aber wie auch immer, wenn Sie uns DI zum ersten Mal möchten, sollten Sie diesen http://misko.hevery.com/2008/10/21/dependency-injection-myth-reference-passing/ Artikel lesen (darauf hingewiesen von @megadan) durch sehr sorgfältige Beachtung des Gesetzes von Demeter (LoD) oder des Grundsatzes des geringsten Wissens .
quelle
Es ist nicht ungewöhnlich, dass Teile einer Codebasis als Eckpfeiler oder als Basisklasse betrachtet werden, aber das rechtfertigt nicht, dass der Lebenszyklus als Singleton diktiert wird.
Programmierer verlassen sich oft auf das Singleton-Muster als Mittel der Bequemlichkeit und puren Faulheit, anstatt sich auf eine alternative Herangehensweise zu beschränken und ein bisschen wortreicher zu sein und in einem expliziten Stil Objektbeziehungen zwischen einander zu erzwingen.
Fragen Sie sich also, was einfacher zu warten ist.
Wenn Sie wie ich sind, ist es mir lieber, eine Header-Datei zu öffnen und Konstruktorargumente und öffentliche Methoden kurz zu überfliegen, um zu sehen, welche Abhängigkeiten ein Objekt benötigt, anstatt Hunderte von Codezeilen in einer Implementierungsdatei untersuchen zu müssen.
Wenn Sie ein Objekt zu einem Singleton gemacht und später erkannt haben, dass Sie in der Lage sein müssen, mehrere Instanzen des Objekts zu unterstützen, stellen Sie sich die umfassenden Änderungen vor, die erforderlich sind, wenn Sie diese Klasse in Ihrer gesamten Codebasis ziemlich häufig verwendet haben. Die bessere Alternative besteht darin, Abhängigkeiten explizit anzugeben, auch wenn das Objekt nur einmal zugeordnet wird und wenn die Notwendigkeit besteht, mehrere Instanzen zu unterstützen, können Sie dies ohne Bedenken tun.
Wie bereits erwähnt, verschleiert die Verwendung des Singleton-Musters auch die Kopplung, was häufig auf ein schlechtes Design und eine hohe Kohäsion zwischen den Modulen hinweist. Eine solche Kohäsion ist normalerweise unerwünscht und führt zu sprödem Code, der sich nur schwer aufrechterhalten lässt.
quelle
Singleton ist ein berühmtes Muster, aber es ist gut zu wissen, welchen Zwecken es dient und welche Vor- und Nachteile es hat.
Es macht wirklich Sinn, wenn überhaupt keine Beziehung besteht .
Wenn Sie mit Ihrer Komponente mit einem völlig anderen Objekt umgehen können (keine starke Abhängigkeit) und dasselbe Verhalten erwarten, ist der Singleton möglicherweise eine gute Wahl.
Wenn Sie andererseits eine kleine Funktionalität benötigen , die nur zu bestimmten Zeiten verwendet wird, sollten Sie es vorziehen , Referenzen zu übergeben .
Wie Sie bereits erwähnt haben Singletons keine lebenslange Kontrolle. Aber der Vorteil ist, dass sie eine verzögerte Initialisierung haben.
Wenn Sie diesen Singleton in Ihrer Anwendungslebensdauer nicht aufrufen, wird er nicht instanziiert. Aber ich bezweifle, dass Sie Singletons bauen und sie nicht verwenden.
Sie müssen einen Singleton wie einen Dienst anstelle einer Komponente sehen .
Singletons funktioniert, sie haben nie eine Anwendung unterbrochen. Aber ihre Mängel (die vier, die du angegeben hast) sind Gründe, warum du keinen machst.
quelle
Unbekanntheit ist nicht der Grund, warum Singletons vermieden werden müssen.
Es gibt viele gute Gründe, warum Singleton notwendig oder unvermeidbar ist. Spiel-Frameworks verwenden häufig Singletons, da dies eine unvermeidliche Folge der Verwendung einer einzigen Stateful-Hardware ist. Es macht keinen Sinn, diese Hardware jemals mit mehreren Instanzen ihrer jeweiligen Handler steuern zu wollen. Die Grafikoberfläche ist eine externe Stateful-Hardware, und das versehentliche Initialisieren einer zweiten Kopie des Grafik-Subsystems ist geradezu katastrophal, da sich die beiden Grafik-Subsysteme jetzt gegenseitig darum streiten, wer wann zeichnen darf und sich unkontrolliert überschreiben. Ähnlich wie beim Ereigniswarteschlangensystem streiten sie sich darum, wer die Mausereignisse auf nicht deterministische Weise abruft. Wenn es sich um eine zustandsbehaftete externe Hardware handelt, in der es nur eine gibt, ist Singleton unvermeidlich, um Konflikte zu vermeiden.
Ein weiterer Ort, an dem Singleton sinnvoll ist, sind Cache-Manager. Caches sind ein Sonderfall. Sie sollten nicht wirklich als Singleton betrachtet werden, selbst wenn sie dieselben Techniken wie Singletons anwenden, um am Leben zu bleiben und für immer zu leben. Caching-Dienste sind transparente Dienste. Sie sollen das Verhalten des Programms nicht ändern. Wenn Sie also den Cache-Dienst durch einen Null-Cache ersetzen, sollte das Programm weiterhin funktionieren, außer dass es nur langsamer ausgeführt wird. Der Hauptgrund, warum Cache-Manager eine Ausnahme von Singleton darstellen, ist, dass es nicht sinnvoll ist, einen Cache-Dienst vor dem Herunterfahren der Anwendung selbst herunterzufahren, da dadurch auch die zwischengespeicherten Objekte verworfen werden, was den Punkt zunichte macht, dass es sich um einen handelt Singleton.
Das sind gute Gründe, warum diese Dienste Singletons sind. Keiner der guten Gründe, Singletons zu haben, trifft jedoch auf die von Ihnen aufgelisteten Klassen zu.
Das sind keine Gründe für Singletons. Das sind Gründe für Globals. Dies ist auch ein Zeichen für schlechtes Design, wenn Sie viele Dinge in verschiedenen Systemen austauschen müssen. Das Weitergeben von Dingen weist auf eine hohe Kopplung hin, die durch ein gutes OO-Design verhindert werden kann.
Wenn Sie sich nur die Liste der Klassen in Ihrem Beitrag ansehen, von denen Sie glauben, dass sie Singletons sein müssen, kann ich sagen, dass die Hälfte davon eigentlich keine Singletons sein sollte und die andere Hälfte den Anschein hat, dass sie gar nicht erst da sein sollten. Dass Sie Objekte weitergeben müssen, liegt eher an der fehlenden Kapselung als an den tatsächlichen guten Anwendungsfällen für Singletons.
Lassen Sie uns Ihre Klassen Stück für Stück betrachten:
Nur von den Klassennamen würde ich sagen, dass diese Klassen nach anämischem Domänenmodell- Antipattern riechen. Anämische Objekte führen normalerweise dazu, dass viele Daten weitergegeben werden müssen, was die Kopplung erhöht und den Rest des Codes umständlich macht. Außerdem verbirgt die anämische Klasse die Tatsache, dass Sie wahrscheinlich immer noch prozedural denken, anstatt die Objektorientierung zum Einkapseln von Details zu verwenden.
Warum müssen diese Klassen Singletons sein? Es scheint mir, dass dies kurzlebige Klassen sein sollten, die aufgerufen werden sollten, wenn sie benötigt werden, und dekonstruiert werden sollten, wenn der Benutzer den Kauf abschließt oder wenn die Anzeigen nicht mehr gezeigt werden müssen.
Mit anderen Worten, ein Konstruktor und eine Serviceschicht? Warum gibt es in dieser Klasse überhaupt keine klaren Grenzen und keinen klaren Zweck?
Warum ist der Spielerfortschritt von der Spielerklasse getrennt? Die Player-Klasse sollte wissen, wie sie ihren eigenen Fortschritt verfolgt. Wenn Sie die Fortschrittsverfolgung in einer anderen Klasse als der Player-Klasse implementieren möchten, um die Verantwortung zu trennen, sollte PlayerProgress hinter Player stehen.
Ich kann nicht weiter darauf eingehen, ohne zu wissen, was diese Klasse tatsächlich tut, aber eins ist klar: Diese Klasse hat einen schlechten Namen.
Dies scheinen die einzigen Klassen zu sein, in denen Singleton sinnvoll sein kann, da die sozialen Verbindungsobjekte eigentlich nicht Ihre Objekte sind. Die sozialen Verbindungen sind nur ein Schatten von externen Objekten, die in einem entfernten Dienst leben. Mit anderen Worten, dies ist eine Art Cache. Stellen Sie nur sicher, dass Sie den Caching-Teil klar vom Service-Teil des externen Dienstes trennen, da der letztere Teil kein Singleton sein muss.
Eine Möglichkeit, die Weitergabe von Instanzen dieser Klassen zu vermeiden, ist die Verwendung der Nachrichtenübergabe. Anstatt Instanzen dieser Klassen direkt aufzurufen, senden Sie stattdessen asynchron eine an den Analytic- und SocialConnection-Dienst adressierte Nachricht, und diese Dienste sind abonniert, um diese Nachrichten zu empfangen und auf die Nachricht zu reagieren. Da es sich bei der Ereigniswarteschlange bereits um einen Singleton handelt, müssen Sie bei der Kommunikation mit einem Singleton keine tatsächliche Instanz übergeben, indem Sie den Anruf mit einem Trampolin versehen.
quelle
Singletons sind für mich IMMER schlecht.
In meiner Architektur habe ich eine GameServices-Sammlung erstellt, die von der Spielklasse verwaltet wird. Ich kann nach Bedarf suchen, um dieser Sammlung etwas hinzuzufügen oder daraus zu entfernen.
Wenn Sie sagen, dass etwas in globaler Reichweite ist, sagen Sie in den Beispielen, die Sie oben angegeben haben, im Grunde, dass "dieses Ding Gott ist und keinen Meister hat". Ich würde sagen, dass die meisten davon entweder Helferklassen oder GameServices sind.
Aber das ist nur mein Design in meinem Motor, Ihre Situation kann anders sein.
Direkter ausgedrückt ... Der einzige wirkliche Singleton ist das Spiel, da es jeden Teil des Codes besitzt.
Diese Antwort basiert auf der subjektiven Natur der Frage und basiert rein auf meiner Meinung, sie ist möglicherweise nicht für alle Entwickler da draußen korrekt.
Bearbeiten: Wie gewünscht ...
Ok, das ist aus meinem Kopf, da ich im Moment meinen Code nicht vor mir habe, aber im Grunde genommen ist die Game-Klasse die Wurzel eines jeden Spiels, die wirklich ein Singleton ist ... das muss es sein!
Also habe ich folgendes hinzugefügt ...
Jetzt habe ich auch eine Systemklasse (statisch), die alle meine Singletons aus dem gesamten Programm enthält (enthält sehr wenig, Spiel- und Konfigurationsoptionen und das wars auch schon) ...
Und Nutzung ...
Von überall kann ich sowas machen
Dieser Ansatz bedeutet, dass mein Spiel Dinge "besitzt", die normalerweise als "Singleton" angesehen werden, und ich kann die Basis-GameService-Klasse beliebig erweitern. Ich habe Schnittstellen wie IUpdateable, nach denen mein Spiel sucht, und rufe alle Dienste auf, die es je nach Bedarf für bestimmte Situationen implementieren.
Ich habe auch Dienste, die andere Dienste für spezifischere Szenarien erben / erweitern.
Beispielsweise ...
Ich habe einen Physikdienst, der meine Physiksimulationen verarbeitet. Abhängig von der Szene kann die Physiksimulation jedoch etwas anderes Verhalten erfordern.
quelle
var tService = Sys.Game.getService<T>(); tService.Something();
anstatt dasgetService
Teil zu überspringen und direkt darauf zuzugreifen?Ein wesentlicher Bestandteil der Singleton-Eliminierung, den Gegner von Singleton häufig nicht berücksichtigen, ist das Hinzufügen zusätzlicher Logik, um den weitaus komplizierteren Zustand zu bewältigen, den Objekte einkapseln, wenn Singletons Instanzwerten weichen. Selbst das Definieren des Zustands eines Objekts nach dem Ersetzen eines Singletons durch eine Instanzvariable kann schwierig sein. Programmierer, die Singletons durch Instanzvariablen ersetzen, sollten jedoch darauf vorbereitet sein, damit umzugehen.
Vergleichen Sie beispielsweise Java
HashMap
mit .NETDictionary
. Beide sind sich recht ähnlich, haben aber einen wesentlichen Unterschied: JavaHashMap
ist fest programmiertequals
und verwendethashCode
(effektiv einen Singleton-Komparator), während .NETDictionary
eine Instanz vonIEqualityComparer<T>
als Konstruktorparameter akzeptieren kann. Die Laufzeitkosten für die Aufbewahrung der SoftwareIEqualityComparer
sind minimal, die damit verbundene Komplexität jedoch nicht.Da jede
Dictionary
Instanz ein kapseltIEqualityComparer
, ist ihr Status nicht nur eine Zuordnung zwischen den tatsächlich enthaltenen Schlüsseln und den zugehörigen Werten, sondern eine Zuordnung zwischen den durch die Schlüssel und den Komparator definierten Äquivalenzsätzen und den zugehörigen Werten. Wenn zwei Instanzend1
undd2
derDictionary
Halt Verweise auf den gleichenIEqualityComparer
, wird es möglich sein , eine neue Instanz zu erzeugen ,d3
so dass für jedex
,d3.ContainsKey(x)
wird gleichd1.ContainsKey(x) || d2.ContainsKey(x)
. Wenn jedoch Verweise auf verschiedene willkürliche Gleichheitsvergleicher vorhanden sindd1
und vorhanden seind2
können, gibt es im Allgemeinen keine Möglichkeit, sie zusammenzuführen, um sicherzustellen, dass diese Bedingung erfüllt ist.Das Hinzufügen von Instanzvariablen, um Singletons zu eliminieren, kann dazu führen, dass Code spröder wird, als dies bei einem Singleton der Fall gewesen wäre, wenn die möglichen neuen Zustände, die durch diese Instanzvariablen impliziert werden könnten, nicht ordnungsgemäß berücksichtigt werden. Wenn jede Objektinstanz über ein separates Feld verfügt, die Funktionen jedoch nur dann fehlerhaft sind, wenn alle dieselbe Objektinstanz identifizieren, können mit den neuen Feldern mehr Probleme auftreten.
quelle