Wann ist ein Zirkelverweis auf einen übergeordneten Zeiger zulässig?

24

Diese Stapelüberlauf- Frage handelt von einem Kind, das über einen Zeiger auf sein Elternteil verweist.

Die Kommentare waren anfangs ziemlich kritisch, weil das Design eine schreckliche Idee war.

Ich verstehe, dass dies wahrscheinlich nicht die beste Idee im Allgemeinen ist. Von einer allgemeinen Faustregel aus scheint es fair zu sein zu sagen: "Tu das nicht!"

Ich frage mich jedoch, unter welchen Bedingungen Sie so etwas tun müssten. Diese Frage hier und die dazugehörigen Antworten / Kommentare deuten sogar darauf hin, dass Grafiken so etwas nicht tun sollten.

Enderland
quelle
1
Die Frage, die Sie verknüpft haben, scheint ziemlich umfassend zu sein.
Leichtigkeit Rennen mit Monica
4
@LightnessRacesinOrbit "Mach das nicht" ist nicht wirklich nützlich, um zu verstehen, warum .
Enderland
2
Ich sehe viel mehr als "Mach das nicht". Ich sehe Vor- und Nachteile, die von mehreren Experten diskutiert wurden.
Leichtigkeit Rennen mit Monica
1
Möglicherweise haben Sie eine bidirektionale Liste, die durchlaufen werden muss, eine Art Umlaufpuffer, möglicherweise stellen Sie zwei Teile einer zusammenhängenden Straße in einem Spiel dar. Wenn Sie etwas Umlaufendes darstellen müssen, ist dies möglicherweise eine gute Idee.
Rhughes
2
Meine pragmatische Faustregel lautet: "Kann ein Kind ohne Eltern existieren?". (Wenn Sie XmlDocuments und seine Knoten berücksichtigen: Ein Knoten kann nicht ohne den Baumkontext eines Dokuments existieren. Es ist ein Unsinn.) Wenn die Antwort nein ist, sind bidirektionale Verknüpfungen in Ordnung: Sie haben zwei Objekte, die nur zusammen existieren können. Wenn Objekte können unabhängig voneinander existieren, dann entferne ich eine dieser beiden Verbindungen.
Mikalai

Antworten:

43

Der Schlüssel hierbei ist nicht, ob zwei Objekte Zirkelverweise haben, sondern ob diese Verweise das Eigentum aneinander anzeigen .

Zwei Objekte können sich nicht gegenseitig "besitzen": Dies führt zu einem unlösbaren Dilemma für die Initialisierungs- und Löschreihenfolge. Eine muss eine optionale Referenz sein oder auf andere Weise anzeigen, dass ein Objekt die Lebensdauer des anderen Objekts nicht verwaltet.

Stellen Sie sich eine doppelt verknüpfte Liste vor: Zwei Knoten sind miteinander verbunden, aber keiner "besitzt" den anderen (die Liste besitzt beide). Dies bedeutet, dass keiner der Knoten Speicher für den anderen reserviert oder anderweitig für das Identitäts- oder Lebensdauermanagement des anderen verantwortlich ist.

Bäume haben eine ähnliche Beziehung, obwohl Knoten in einem Baum Kinder zuweisen können und Eltern Kinder besitzen. Die Verbindung von einem Kind zu einem Elternteil hilft beim Durchqueren, definiert aber wiederum nicht die Eigentümerschaft.

In den meisten OO-Entwürfen impliziert ein Verweis auf ein anderes Objekt als Datenelement eines Objekts den Besitz. Angenommen, wir haben die Klassen Auto und Motor. Keiner von beiden ist für sich genommen sehr nützlich. Wir können sagen, dass diese Objekte voneinander abhängen : Sie erfordern die Anwesenheit des anderen, um nützliche Arbeit zu leisten. Aber wem "gehört" der andere? In diesem Fall würden wir sagen, dass Auto Motor besitzt, weil das Auto der "Container" ist, in dem alle Automobilkomponenten leben. Sowohl im OO- als auch im Real-World-Design ist das Auto die Summe seiner Teile, und alle diese Teile sind im Kontext des Autos miteinander verbunden. Der Motor hat möglicherweise einen Verweis auf Car oder auf TorqueConverter.

Zirkelbezüge können ein schlechter Gestaltungsgeruch sein, müssen es aber nicht. Bei vorsichtiger Verwendung und korrekter Dokumentation können sie die Verwendung von Datenstrukturen vereinfachen.

Versuchen Sie, einen Baum zu überqueren, ohne dass zwischen Eltern und Kindern Verweise bestehen. Sicher, Sie könnten einen stapelbasierten Ansatz finden, der spröde und komplex ist, oder Sie könnten den referenzbasierten Ansatz verwenden, der trivial einfach ist.


quelle
16

Bei einem solchen Entwurf sind mehrere Aspekte zu berücksichtigen:

  • die strukturellen Abhängigkeiten
  • das Eigentumsverhältnis (dh Zusammensetzung im Vergleich zu einer anderen Art von Assoziation)
  • die Navigation braucht

Strukturabhängigkeit zwischen Klassen:

Wenn Sie Komponentenklassen wiederverwenden möchten, sollten Sie unnötige Abhängigkeiten und solche geschlossenen kreisförmigen Strukturen vermeiden.

Trotzdem sind manchmal zwei Klassen konzeptionell stark miteinander verbunden. In diesem Fall ist die Vermeidung von Abhängigkeiten keine echte Option. Beispiel: Ein Baum und seine Blätter oder allgemeiner ein Verbundwerkstoff und seine Bestandteile .

Eigentum an Objekten:

Besitzt ein Objekt das andere? Oder anders gesagt: Wenn ein Objekt zerstört wird, soll auch das andere zerstört werden?

Dieses Thema wurde von Snowman eingehend behandelt, daher werde ich es hier nicht ansprechen.

Navigationsbedarf zwischen Objekten:

Ein letztes Problem ist das Navigationsbedürfnis. Nehmen wir mein Lieblingsbeispiel, das zusammengesetzte Designmuster der Gang of Four .

Gamma & al. Erwähnen Sie explizit die mögliche Notwendigkeit eines expliziten übergeordneten Verweises: "Das Beibehalten des Verweises von untergeordneten Komponenten auf deren übergeordnete Komponente kann das Durchlaufen und Verwalten einer zusammengesetzten Struktur vereinfachen. " kann die Operationen auf exponentielle Weise erheblich verlangsamen. Eine direkte Referenz, auch zirkuläre, kann die Manipulation Ihrer Verbundwerkstoffe erheblich erleichtern.

Ein Beispiel könnte ein grafisches Modell eines elektronischen Systems sein. Eine zusammengesetzte Struktur könnte die elektronischen Karten, Schaltkreise, Elemente darstellen. Zum Anzeigen und Bearbeiten des Modells benötigen Sie einige geometrische Proxys in einer GUI-Ansicht. Es ist dann sicherlich viel einfacher, vom vom Benutzer ausgewählten GUI-Element zur Komponente zu navigieren, um herauszufinden, welches das übergeordnete Element und mit welchem ​​das verwandte Bruder- / Schwesterelement ist, als eine Suche von oben nach unten zu starten.

Natürlich müssen Sie, wie Gamma & al betont hat, die Invarienten der Kreisbeziehung sicherstellen. Dies kann schwierig sein, wie die SO-Frage, auf die Sie sich beziehen, gezeigt hat. Aber es ist perfekt handhabbar und sicher.

Fazit

Das Navigationsbedürfnis darf nicht unterschätzt werden. Nicht umsonst hat UML dies in der Modellierungsnotation explizit angesprochen. Und ja, es gibt eine durchaus gültige Situation, in der Zirkelverweise benötigt werden.

Der einzige Punkt ist, dass Menschen manchmal dazu neigen, zu schnell in eine solche Richtung zu gehen. Es lohnt sich also, alle drei Aspekte zu berücksichtigen, bevor Sie sich entscheiden, ob Sie es wollen oder nicht.

Christophe
quelle
1
Diese Antwort ist meiner Meinung nach stark unterbewertet. Das OP fragte: "Wenn es bereits eine Eltern-Kind-Beziehung gibt, wann ist es in Ordnung, dies durch einen Zirkelverweis umzusetzen?". Damit ist die Struktur und die "Eigentümerschaft" (im hier genannten Sinne) bereits klar. Das heißt, die einzigen Kriterien für das Hinzufügen von Referenzen auf der einen oder anderen Seite sind die Fragen nach "Navigationsbedürfnissen" und "unabhängiger Wiederverwendung des Kindes".
Doc Brown
@DocBrown - Sie können dieser Frage immer ein Kopfgeld geben, wenn sie berechtigt ist. :-)
1
@GlenH7: das würde dieser Antwort nicht mehr Stimmen geben, nur Schneemanns Antwort (für die ich denke, dass es etwas den Sinn der Frage verfehlt).
Doc Brown
1
@ DocBrown - Aber die Repz, Mann, die Repz!
... und wenn Sie Sichtbarkeitsoptionen wie öffentlich-privat-geschützt hinzufügen, erhalten Sie 9 verschiedene Optionen :)
mikalai
8

In der Regel sind Zirkelverweise eine sehr schlechte Idee, da sie zirkuläre Abhängigkeiten bedeuten. Sie wissen wahrscheinlich bereits, warum zirkuläre Abhängigkeiten schlecht sind, aber der Vollständigkeit halber ist die tl; dr-Version, dass es unmöglich ist, A oder B zu verstehen, ohne dass die Klassen A und B voneinander abhängig sind gleichzeitig die andere Klasse verstehen / reparieren / optimieren / etc. Das führt schnell zu Codebasen, in denen Sie nichts ändern können, ohne alles zu ändern.

Es ist jedoch möglich, einen Zirkelverweis zu haben, ohne böse Zirkelabhängigkeiten zu erzeugen. Dies funktioniert, solange die Referenz in funktionaler Hinsicht rein optional ist . Damit meine ich, dass Sie es leicht aus den Klassen entfernen können, und sie würden immer noch funktionieren, auch wenn sie zufällig langsamer arbeiten. Der mir bekannte Hauptanwendungsfall für solche zirkulären, keine Abhängigkeiten erzeugenden Referenzen besteht darin, das schnelle Durchlaufen von knotenbasierten Datenstrukturen wie verknüpften Listen, Bäumen und Heaps zu ermöglichen. Grundsätzlich kann jede Operation, die Sie an einer doppelt verknüpften Liste ausführen können, auch an einer einfach verknüpften Liste ausgeführt werden. Es gibt jedoch zufällig einige Operationen (z. B. das Zurückblättern in der Liste), die eine viel bessere Größe haben. O mit der doppelt verknüpften Version.

Ixrec
quelle
6

Der Grund, warum dies normalerweise keine gute Idee ist, liegt darin, dass es gegen das Prinzip der Abhängigkeitsinversion verstößt . Die Leute haben viel ausführlicher darüber geschrieben, als ich in diesem Beitrag adäquat behandeln kann, aber es läuft darauf hinaus, es schwierig zu machen, es zu warten, weil die Kupplung so eng ist. Das Ändern einer Klasse erfordert fast immer eine Änderung der anderen, während Änderungen auf einer Seite der Schnittstelle isoliert sind, wenn die Abhängigkeiten nur in eine Richtung weisen. Wenn beide Klassen auf eine abstrakte Schnittstelle verweisen, ist dies noch besser.

Die einzige Ausnahme besteht darin, dass Sie nicht zwei verschiedene Klassen auf verschiedenen Abstraktionsebenen haben, sondern zwei Knoten derselben Klasse, z. B. in einem Baum, einer doppelt verknüpften Liste usw. Hier handelt es sich eher um eine strukturelle Beziehung als um eine Abstraktionsbeziehung. Zirkelverweise aus Gründen der algorithmischen Effizienz werden in solchen Fällen akzeptiert und sogar empfohlen.

Karl Bielefeldt
quelle
5

[...] schlägt sogar für Graphen vor, so etwas nicht zu tun.

Manchmal müssen Sie nur von einer anderen Datenstruktur als dem Baum auf Dinge von unten zugreifen, während der Baum von oben nach unten auf Dinge zugreifen muss.

Beispielsweise kann ein Quadtree Elemente in einer Vektorgrafiksoftware speichern. Die Auswahl des Benutzers wird jedoch in einer separaten Auswahlliste von Vektorelementreferenzen / -zeigern gespeichert. Wenn der Benutzer diese Auswahl löschen möchte, muss der Quadtree aktualisiert werden, und da ist es möglicherweise weitaus effizienter, den Baum von unten nach oben zu aktualisieren, und nicht von oben nach unten. Andernfalls müssten Sie für jedes Element von Stamm zu Blatt arbeiten und dann erneut sichern.


quelle
1
Ja, dieses Beispiel ist wirklich angemessen. Genau die Art von Dingen, die ich in meiner Auseinandersetzung mit der Navigation in einem Verbund beschreiben wollte: Der Backpointer beschleunigt die Geschwindigkeit erheblich. Vielen Dank für Ihre interessante Performance-Anekdote und Ihr sehr übersichtliches Diagramm! +1
Christophe
@Christophe Ich mag auch dein Beispiel! Ich war mir nicht sicher, ob ich die Frage tatsächlich richtig beantwortete, da es sich vielleicht mehr um "zirkuläres Eigentum" als nur um Rückverweise handelte, mit denen wir eine Datenstruktur nach oben / hinten durchlaufen konnten. Aber ich habe hauptsächlich auf diesen letzten "Graph" -Teil der Frage geantwortet.
2

Doom 3 enthält ein Beispiel für ein untergeordnetes Objekt mit einem Zeiger auf ein übergeordnetes Objekt. Insbesondere verwendet es aufdringliche Listen verwendet . Zusammenfassend ist eine aufdringliche Liste wie eine verknüpfte Liste, außer dass jeder Knoten einen Zeiger auf die Liste selbst enthält.

Vorteile:

  • Wenn Objekte in mehreren Listen gleichzeitig existieren können, muss der Speicher für die Listenknoten nur einmal zugewiesen und freigegeben werden.

  • Wenn ein Objekt zerstört werden muss, können Sie es einfach aus allen Listen entfernen, in denen es sich befindet, ohne jede Liste linear durchsuchen zu müssen.

Ich denke, dies ist ein ziemlich spezifisches Szenario, aber wenn ich Ihre Frage verstehe, ist es ein Beispiel für eine akzeptable Verwendung eines untergeordneten Objekts, das einen Zeiger auf das übergeordnete Objekt enthält.

user2023861
quelle