Natürlich kommt es auf die Situation an. Wenn jedoch ein Objekt oder System mit niedrigerem Hebel mit einem System höherer Ebene kommuniziert, sollten Rückrufe oder Ereignisse dem Behalten eines Zeigers auf ein Objekt höherer Ebene vorgezogen werden?
Zum Beispiel haben wir eine world
Klasse, die eine Mitgliedsvariable hat vector<monster> monsters
. Wenn die monster
Klasse mit dem kommuniziert world
, sollte ich dann lieber eine Rückruffunktion verwenden oder einen Zeiger auf die world
Klasse innerhalb der monster
Klasse haben?
architecture
software-engineering
hidayat
quelle
quelle
Antworten:
Es gibt drei Möglichkeiten, wie eine Klasse mit einer anderen sprechen kann, ohne eng daran gebunden zu sein:
Die drei sind eng miteinander verwandt. Ein Ereignissystem ist in vielerlei Hinsicht nur eine Liste von Rückrufen. Ein Rückruf ist mehr oder weniger eine Schnittstelle mit einer einzigen Methode.
In C ++ verwende ich selten Rückrufe:
C ++ bietet keine gute Unterstützung für Rückrufe, bei denen der
this
Zeiger beibehalten wird. Daher ist es schwierig, Rückrufe in objektorientiertem Code zu verwenden.Ein Rückruf ist im Grunde eine nicht erweiterbare Ein-Methoden-Schnittstelle. Mit der Zeit stelle ich fest, dass ich fast immer mehr als eine Methode benötige, um diese Schnittstelle zu definieren, und ein einziger Rückruf ist selten genug.
In diesem Fall würde ich wahrscheinlich eine Schnittstelle machen. In Ihrer Frage geben Sie nicht an, mit was
monster
tatsächlich kommuniziert werden mussworld
. Wenn ich eine Vermutung anstellen würde, würde ich so etwas tun:Die Idee hier ist, dass Sie nur
IWorld
das Nötigste eingeben (was ein beschissener Name ist), auf das zugegriffen werdenMonster
muss. Sein Blick auf die Welt sollte so eng wie möglich sein.quelle
Verwenden Sie keine Rückruffunktion, um zu verbergen, welche Funktion Sie aufrufen. Wenn Sie eine Rückruffunktion einfügen und dieser Rückruffunktion nur eine einzige Funktion zugewiesen ist, wird die Kopplung überhaupt nicht unterbrochen. Sie maskieren es nur mit einer anderen Abstraktionsebene. Sie gewinnen nichts (außer vielleicht Kompilierungszeit), aber Sie verlieren an Klarheit.
Ich würde es nicht gerade als Best Practice bezeichnen, aber es ist ein gängiges Muster, Entitäten zu haben, die in etwas enthalten sind, um einen Zeiger auf das übergeordnete Element zu haben.
Abgesehen davon könnte es sich lohnen, das Schnittstellenmuster zu verwenden, um Ihren Monstern eine begrenzte Teilmenge an Funktionen zu geben, die sie auf der Welt aufrufen können.
quelle
Im Allgemeinen versuche ich, bidirektionale Links zu vermeiden, aber wenn ich sie haben muss, stelle ich absolut sicher, dass es eine Methode gibt, um sie zu erstellen und eine, um sie zu brechen, damit es nie zu Inkonsistenzen kommt.
Oft können Sie die bidirektionale Verknüpfung vollständig vermeiden, indem Sie bei Bedarf Daten übergeben. Ein triviales Refactoring besteht darin, es so zu gestalten, dass das Monster keine Verbindung zur Welt hat, sondern die Welt anhand der Monstermethoden weitergibt, die es benötigen. Noch besser ist es, nur eine Schnittstelle für die Teile der Welt zu übergeben, die das Monster unbedingt benötigt, was bedeutet, dass sich das Monster nicht auf die konkrete Implementierung der Welt verlässt. Dies entspricht dem Prinzip der Schnittstellentrennung und dem Prinzip der Abhängigkeitsinversion , führt jedoch nicht zu der Einführung der überschüssigen Abstraktion, die manchmal mit Ereignissen, Signalen + Slots usw. erzielt werden kann.
In gewisser Weise können Sie argumentieren, dass die Verwendung eines Rückrufs eine sehr spezialisierte Mini-Schnittstelle ist, und das ist in Ordnung. Sie müssen entscheiden, ob Sie Ihre Ziele mit einer Sammlung von Methoden in einem Schnittstellenobjekt oder mehreren sortierten Methoden in verschiedenen Rückrufen sinnvoller erreichen können.
quelle
Ich versuche zu vermeiden, dass enthaltene Objekte ihren Container aufrufen, weil ich finde, dass dies zu Verwirrung führt, zu einfach zu rechtfertigen ist, überbeansprucht wird und Abhängigkeiten entstehen, die nicht verwaltet werden können.
Meiner Meinung nach besteht die ideale Lösung darin, dass die Klassen höherer Ebenen intelligent genug sind, um die Klassen niedrigerer Ebenen zu verwalten. Zum Beispiel ist die Welt, die weiß, ob es zu einer Kollision zwischen einem Monster und einem Ritter gekommen ist, ohne dass einer von beiden etwas über den anderen weiß, auch besser als das Monster, das die Welt fragt, ob es mit einem Ritter kollidiert.
Eine andere Option in Ihrem Fall, ich würde wahrscheinlich herausfinden, warum die Monsterklasse etwas über die Weltklasse wissen muss, und Sie werden höchstwahrscheinlich feststellen, dass es etwas in der Weltklasse gibt, das in eine eigene Klasse aufgeteilt werden kann, die es macht Sinn für die Monsterklasse zu wissen.
quelle
Ohne Ereignisse werden Sie natürlich nicht sehr weit kommen, aber bevor Sie überhaupt anfangen, ein Ereignissystem zu schreiben (und zu entwerfen), sollten Sie sich die eigentliche Frage stellen: Warum sollte das Monster mit der Weltklasse kommunizieren? Sollte es wirklich?
Nehmen wir eine "klassische" Situation, ein Monster, das einen Spieler angreift.
Monster greift an: Die Welt kann die Situation, in der sich ein Held neben einem Monster befindet, sehr gut identifizieren und dem Monster sagen, dass es angreifen soll. Die Funktion in Monster wäre also:
Aber die Welt (die das Monster bereits kennt) muss vom Monster nicht erkannt werden. Tatsächlich kann das Monster die Existenz der Klassenwelt ignorieren, was wahrscheinlich besser ist.
Das Gleiche gilt, wenn sich ein Monster bewegt (ich lasse Subsysteme die Kreatur holen und die Bewegungsberechnung / -absicht dafür übernehmen, wobei das Monster nur eine Tasche mit Daten ist, aber viele Leute würden sagen, dass dies keine echte OOP ist).
Mein Punkt ist: Ereignisse (oder Rückrufe) sind natürlich großartig, aber sie sind nicht die einzige Antwort auf jedes Problem, mit dem Sie konfrontiert werden.
quelle
Wann immer ich kann, versuche ich, die Kommunikation zwischen Objekten auf ein Anforderungs- und Antwortmodell zu beschränken. Es gibt eine implizite Teilreihenfolge für die Objekte in meinem Programm, so dass zwischen zwei beliebigen Objekten A und B eine Möglichkeit für A besteht, eine Methode von B direkt oder indirekt aufzurufen, oder für B, eine Methode von A direkt oder indirekt aufzurufen , aber es ist A und B niemals möglich, sich gegenseitig die Methoden aufzurufen. Manchmal möchten Sie natürlich eine Rückwärtskommunikation mit dem Aufrufer einer Methode haben. Es gibt ein paar Möglichkeiten, wie ich das gerne mache, und keine davon ist ein Rückruf.
Eine Möglichkeit besteht darin, mehr Informationen in den Rückgabewert des Methodenaufrufs aufzunehmen. Dies bedeutet, dass der Clientcode entscheiden kann, was damit geschehen soll, nachdem die Prozedur die Kontrolle an ihn zurückgegeben hat.
Die andere Möglichkeit besteht darin, ein gemeinsames untergeordnetes Objekt aufzurufen. Das heißt, wenn A eine Methode für B aufruft und B einige Informationen an A übermitteln muss, ruft B eine Methode für C auf, wobei A und B beide C aufrufen können, C jedoch A oder B nicht aufrufen kann. Objekt A wäre dann verantwortlich dafür, dass die Informationen von C abgerufen werden, nachdem B die Kontrolle an A zurückgegeben hat. Beachten Sie, dass dies nicht wirklich grundlegend anders ist als der erste von mir vorgeschlagene Weg. Objekt A kann die Informationen immer noch nur von einem Rückgabewert abrufen. Keine der Methoden von Objekt A wird von B oder C aufgerufen. Eine Variation dieses Tricks besteht darin, C als Parameter an die Methode zu übergeben, aber die Einschränkungen für die Beziehung von C zu A und B gelten weiterhin.
Die wichtige Frage ist nun, warum ich darauf bestehe, die Dinge so zu machen. Es gibt drei Hauptgründe:
self
während der Ausführung einer Methode ändern kann , diese eine und keine andere Methode ist. Dies ist die gleiche Art von Argumentation, die dazu führen könnte, dass Mutexe auf gleichzeitige Objekte gesetzt werden.Ich bin nicht gegen alle Verwendungszwecke von Rückrufen. In Übereinstimmung mit meiner Richtlinie, niemals den Aufrufer aufzurufen, ändert der Rückruf möglicherweise nicht den internen Status von A, wenn ein Objekt A eine Methode für B aufruft und einen Rückruf an B übergibt, und dies schließt die von A und dem gekapselten Objekte ein Objekte im Kontext von A. Mit anderen Worten, der Rückruf kann nur Methoden für Objekte aufrufen, die ihm von B gegeben wurden. Der Rückruf unterliegt praktisch denselben Einschränkungen wie B.
Ein letztes loses Ende ist, dass ich den Aufruf jeder reinen Funktion erlauben werde, unabhängig von dieser Teilreihenfolge, über die ich gesprochen habe. Reine Funktionen unterscheiden sich ein wenig von Methoden darin, dass sie sich nicht ändern können oder von veränderlichen Zuständen oder Nebenwirkungen abhängen. Sie müssen sich also keine Sorgen machen, dass sie die Dinge verwirren.
quelle
Persönlich? Ich benutze nur einen Singleton.
Ja, okay, schlechtes Design, nicht objektorientiert usw. Weißt du was? Es ist mir egal . Ich schreibe ein Spiel, kein Technologie-Schaufenster. Niemand wird mich nach dem Code bewerten. Der Zweck ist es, ein Spiel zu machen, das Spaß macht, und alles, was mir im Weg steht, wird zu einem Spiel führen, das weniger Spaß macht.
Wirst du jemals zwei Welten gleichzeitig haben? Könnte sein! Vielleicht wirst du. Aber wenn Sie jetzt nicht an diese Situation denken können, werden Sie es wahrscheinlich nicht tun.
Also meine Lösung: Machen Sie einen World Singleton. Funktionen darauf aufrufen. Sei fertig mit dem ganzen Durcheinander. Du könntest jeder einzelnen Funktion einen zusätzlichen Parameter übergeben - und keinen Fehler machen, das führt dahin. Oder Sie könnten einfach Code schreiben, der funktioniert.
Um dies zu tun, ist ein wenig Disziplin erforderlich, um die Dinge zu bereinigen, wenn es chaotisch wird (das ist "wann", nicht "wenn"), aber es gibt keine Möglichkeit, zu verhindern, dass Code chaotisch wird - entweder haben Sie das Spaghetti-Problem oder die Tausenden -Layer-of-Abstraction-Problem. Zumindest schreiben Sie auf diese Weise keine großen Mengen an unnötigem Code.
Und wenn Sie sich entscheiden, keinen Singleton mehr zu wollen, ist es normalerweise ziemlich einfach, ihn loszuwerden. Nimmt einige Arbeit in Anspruch, erfordert das Weitergeben einer Milliarde Parameter, aber diese Parameter müssten Sie trotzdem weitergeben.
quelle