Zirkuläre Klassenabhängigkeit

12

Ist es schlecht, zwei Klassen zu haben, die sich gegenseitig brauchen?

Ich schreibe ein kleines Spiel, in dem ich eine GameEngineKlasse habe, die ein paar GameStateObjekte hat. Um auf verschiedene Rendering-Methoden zugreifen zu können, müssen diese GameStateObjekte auch die GameEngineKlasse kennen - es handelt sich also um eine zirkuläre Abhängigkeit.

Würdest du das schlechtes Design nennen? Ich frage nur, weil ich nicht ganz sicher bin und ich diese Dinge derzeit noch umgestalten kann.

shad0w
quelle
2
Ich wäre sehr überrascht, wenn die Antwort ja wäre.
Doppelgreener
Da es zwei Fragen gibt, die logisch getrennt sind, lautet die Antwort auf eine von ihnen ja. Warum möchten Sie einen Zustand entwerfen, der tatsächlich etwas bewirkt? Sie sollten einen Manager / Controller haben, der den Status prüft und die Aktion ausführt. Es ist etwas verwirrend, zuzulassen, dass ein Statusobjekttyp die Zuständigkeiten eines anderen übernimmt. Wenn Sie es tun möchten, ist C ++ Ihr Freund .
Teodron
Meine GameState-Klassen sind Dinge wie das Intro, das eigentliche Spiel, ein Menü und so weiter. Die GameEngine speichert diese Status in einem Stapel, sodass ich einen Status anhalten und ein Menü öffnen oder eine Zwischensequenz abspielen kann. Die GameState-Klassen müssen die GameEngine kennen, da die Engine das Fenster erstellt und über den Renderkontext verfügt .
shad0w
5
Denken Sie daran, alle diese Regeln zielen auf schnell. Schnell auszuführen, schnell zu erstellen, schnell zu warten. Manchmal stimmen diese Aspekte nicht überein, und Sie müssen eine Kosten-Nutzen-Analyse durchführen, um zu entscheiden, wie Sie vorgehen möchten. Wenn Sie auch nur so viel darüber nachdenken und sich etwas einfallen lassen, sind Sie immer noch besser als 90% der Entwickler. Es gibt kein verstecktes Monster, das dich töten wird, wenn du es falsch machst. Er ist eher ein versteckter Mautstellenbetreiber.
DampeS8N

Antworten:

0

Es ist von Natur aus kein schlechtes Design, aber es kann leicht außer Kontrolle geraten. Tatsächlich verwenden Sie dieses Design in Ihren alltäglichen Codes. Beispielsweise weiß vector, dass es sich um den ersten Iterator handelt, und Iteratoren haben Zeiger auf ihren Container.

In Ihrem Fall ist es jetzt viel besser, zwei separate Klassen für GameEnigne und GameState zu haben. Da diese beiden im Grunde genommen unterschiedliche Dinge tun, können Sie später viele Klassen definieren, die GameState erben (wie ein GameState für jede Szene in Ihrem Spiel). Und niemand kann ihre Notwendigkeit, Zugang zu einander zu haben, leugnen. Grundsätzlich führt GameEngine Gamestates aus, daher sollte es einen Zeiger darauf haben. Und GameState verwendet in GameEngine definierte Ressourcen (wie Rendered, Physics Manager usw.).

Sie können und sollten diese beiden Klassen auch nicht ineinander kombinieren, da sie von Natur aus verschiedene Dinge tun und das Kombinieren zu einer sehr großen Klasse führt, die niemand mag.

Bisher wissen wir, dass wir in unserem Design eine zirkuläre Abhängigkeit brauchen. Es gibt mehrere Möglichkeiten, dies sicher zu erstellen:

  1. Wie Sie vorgeschlagen haben, können wir einen Zeiger von jedem in einen anderen platzieren. Dies ist die einfachste Lösung, kann jedoch leicht außer Kontrolle geraten.
  2. Sie können auch das Singleton / Multiton-Design verwenden. Mit dieser Methode sollten Sie Ihre GameEngine-Klasse als Singleton und jeden dieser GameStates als Multiton definieren. Obwohl viele Entwickler sowohl Singleton als auch Multiton als AntiPattern betrachten, bevorzuge ich dieses Design.
  3. Sie können globale Variablen verwenden, sie sind im Grunde dasselbe wie Singleton / multiton, aber sie unterscheiden sich geringfügig darin, dass sie den Programmierer nicht darauf beschränken, Instanzen nach Belieben zu erstellen.

Um seine Antwort abzuschließen, können Sie eine dieser drei Methoden anwenden, aber Sie müssen vorsichtig sein, um keine dieser Methoden zu übertreiben, da, wie ich bereits sagte, all diese Entwürfe wirklich gefährlich sind und leicht zu einem nicht zu wartenden Code führen können.

Ali1S232
quelle
6

Ist es schlecht, zwei Klassen zu haben, die sich gegenseitig brauchen?

Es ist ein bisschen ein Code-Geruch , aber man kann damit gehen. Wenn dies der einfachere und schnellere Weg ist, um Ihr Spiel zum Laufen zu bringen, dann versuchen Sie es. Aber denken Sie daran, denn es besteht eine gute Chance, dass Sie es irgendwann überarbeiten müssen.

Die Sache mit C ++ ist, dass zirkuläre Abhängigkeiten nicht so einfach kompiliert werden können . Daher ist es möglicherweise eine bessere Idee, sie loszuwerden, als Zeit mit der Korrektur Ihrer Kompilierung zu verbringen.

Siehe diese Frage auf SO für ein paar weitere Meinungen.

Würden Sie [mein Design] schlechtes Design nennen?

Nein, es ist immer noch besser, als alles in eine Klasse zu bringen.

Es ist nicht so toll, aber es kommt den meisten Implementierungen, die ich gesehen habe, ziemlich nahe. Normalerweise haben Sie eine Manager-Klasse für Spielzustände ( Vorsicht! ) Und eine Renderer-Klasse, und es ist durchaus üblich, dass es sich um Singletons handelt. Die zirkuläre Abhängigkeit ist also "verborgen", aber möglicherweise vorhanden.

Wie Sie in den Kommentaren erfahren haben, ist es etwas seltsam, dass Klassen mit Spielstatus eine Art Rendering ausführen. Sie sollten nur Statusinformationen enthalten, und das Rendern sollte von einem Renderer oder einer grafischen Komponente von Spielobjekten selbst durchgeführt werden.

Jetzt könnte es das ultimative Design geben. Ich bin gespannt, ob andere Antworten eine gute Idee bringen. Trotzdem sind Sie wahrscheinlich derjenige, der das beste Design für Ihr Spiel findet.

Laurent Couvidou
quelle
Warum sollte es ein Codegeruch sein? Die meisten Objekte mit Eltern-Kind-Beziehung müssen sich gegenseitig sehen.
Kikaimaru
4
Weil es der einfachste Weg ist, nicht zu definieren, welche Klasse für was verantwortlich ist. Dies führt leicht zu zwei Klassen, die so eng miteinander verbunden sind, dass das Hinzufügen von neuem Code in beiden Klassen möglich ist, sodass sie konzeptionell nicht mehr voneinander getrennt sind. Es ist auch eine offene Tür für Spaghetti-Code: Klasse A ruft hierfür Klasse B auf, aber B benötigt diese Informationen von A usw. Also nein, ich würde nicht sagen, dass ein untergeordnetes Objekt etwas über sein übergeordnetes Objekt wissen sollte. Wenn möglich, ist es besser, wenn nicht.
Laurent Couvidou
Das ist ein schlüpfriger Hangfehler. Statische Klassen können auch zu schlechten Dingen führen, aber util-Klassen sind kein Codegeruch. Und ich bezweifle wirklich, dass es einen "guten" Weg gibt, Dinge wie "Scene has SceneNodes" und "SceneNode has reference to Scene" zu machen. Sie können es also nicht zwei verschiedenen Szenen hinzufügen ... Und wo würden Sie aufhören? Ist für A B erforderlich, für B C und für C A ein Codegeruch?
Kikaimaru
Dies ist in der Tat genau wie bei statischen Klassen. Gehen Sie vorsichtig damit um, verwenden Sie es nur, wenn Sie müssen. Jetzt spreche ich aus Erfahrung und ich bin nicht der einzige, der so denkt , aber das ist wirklich eine Ansichtssache. Meiner Meinung nach stinkt dies in dem vom OP gegebenen Kontext tatsächlich.
Laurent Couvidou
In diesem Wikipedia-Artikel wird dieser Fall nicht erwähnt. Und Sie können nicht sagen, dass es sich um einen Fall von "unangemessener Intimität" handelt, bis Sie diese Klassen tatsächlich gesehen haben.
Kikaimaru
6

Es wird oft als schlechtes Design angesehen, zwei Klassen zu haben, die sich direkt aufeinander beziehen, ja. In der Praxis kann es schwieriger sein, den Kontrollfluss durch den Code zu verfolgen, der Besitz von Objekten und deren Lebensdauer können kompliziert sein. Dies bedeutet, dass keine Klasse ohne die andere wiederverwendbar ist von diesen Klassen in einer dritten "Vermittler" -Klasse und so weiter.

Es ist jedoch für 2- Funktionen sehr häufig und das Modell enthält einen Verweis auf ein beobachtbares Objekt, bei dem es sich möglicherweise um eine Ansicht handelt. Wenn sich das Modell ändert, werden alle Observables aufgerufen, und die Ansicht wird implementiert, indem sie das Modell zurückruft und alle aktualisierten Informationen abruft. Der Vorteil hierbei ist, dass das Modell überhaupt nichts über Ansichten weiß (und warum sollte es das tun?) Und in anderen Situationen wiederverwendet werden kann, auch wenn keine Ansichten vorhanden sind. Objekte aufeinander beziehen. Der Unterschied besteht darin, dass die Beziehung in einer Richtung in der Regel abstrakter ist. Beispielsweise kann in einem Model-View-Controller-System das View-Objekt einen Verweis auf das Model-Objekt enthalten und alles darüber wissen, so dass auf alle seine Methoden und Eigenschaften zugegriffen werden kann, damit sich die View mit relevanten Daten füllen kann . Das Model-Objekt enthält möglicherweise einen Verweis auf die Ansicht, sodass die Ansicht aktualisiert werden kann, wenn sich die eigenen Daten geändert haben. Anstatt jedoch das Modell mit einer Ansichtsreferenz zu versehen, die das Modell von der Ansicht abhängig macht, implementiert die Ansicht normalerweise eine Observable-Schnittstelle, häufig mit nur 1Update()Update()Update()

Sie haben eine ähnliche Situation in Ihrem Spiel. Die GameEngine kennt normalerweise GameStates. Der GameState muss jedoch nicht alles über die GameEngine wissen - er benötigt lediglich Zugriff auf bestimmte Rendering-Methoden in der GameEngine. Das sollte einen kleinen Alarm in Ihrem Kopf auslösen, der besagt, dass entweder (a) GameEngine versucht, zu viele Dinge innerhalb einer Klasse zu tun, und / oder (b) GameState nicht die gesamte Spiel-Engine benötigt, sondern nur den darstellbaren Teil.

Drei Lösungsansätze sind:

  • Stellen Sie sicher, dass GameEngine von einer renderbaren Oberfläche abgeleitet wird, und übergeben Sie diese Referenz an den GameState. Wenn Sie das Rendering-Teil jemals überarbeiten, müssen Sie nur sicherstellen, dass es die gleiche Oberfläche beibehält.
  • Zerlegen Sie den Rendering-Teil in ein neues Renderer-Objekt und übergeben Sie ihn stattdessen an GameStates.
  • Lass es so wie es ist. Vielleicht möchten Sie irgendwann doch von GameState aus auf alle GameEngine-Funktionen zugreifen. Bedenken Sie jedoch, dass Software, die einfach zu warten und zu erweitern ist, in der Regel erfordert, dass jede Klasse so wenig wie möglich von außen referenziert wird. definierte Aufgabe ist vorzuziehen.
Kylotan
quelle
0

Es wird allgemein als gute Praxis angesehen, eine hohe Kohäsion bei geringer Kopplung zu haben. Hier einige Links zu Kopplung und Zusammenhalt.

Wikipedia-Kopplung

Wikipedia Zusammenhalt

geringe Kopplung, hohe Kohäsion

Stackoverflow auf Best Practice

Wenn zwei Klassen aufeinander verweisen, ist die Kopplung hoch. Das Google Guice Framework zielt darauf ab, durch Abhängigkeitsinjektion eine hohe Kohäsion bei geringer Kopplung zu erreichen. Ich würde vorschlagen, dass Sie sich ein bisschen mehr mit dem Thema befassen und dann Ihren eigenen Aufruf in Ihrem Kontext machen.

avanderw
quelle