Was ist los mit Zirkelverweisen?


160

Ich war heute in eine Programmierdiskussion involviert, in der ich einige Aussagen machte, die im Grunde genommen davon ausgegangen sind, dass Zirkelverweise (zwischen Modulen, Klassen, was auch immer) im Allgemeinen schlecht sind. Als ich mit meinem Pitch fertig war, fragte mein Kollege: "Was ist los mit Zirkelverweisen?"

Ich habe starke Gefühle dafür, aber es fällt mir schwer, es kurz und konkret auszudrücken. Jede Erklärung, die mir einfällt, stützt sich in der Regel auf andere Elemente, die ich ebenfalls als Axiome betrachte ("kann nicht isoliert verwendet werden, kann also nicht getestet werden", "unbekanntes / undefiniertes Verhalten, da der Status in den beteiligten Objekten mutiert" usw.) .), aber ich würde gerne einen prägnanten Grund dafür hören, warum zirkuläre Verweise schlecht sind, die nicht die Art von Glaubenssprüngen erfordern, die mein eigenes Gehirn macht, nachdem ich im Laufe der Jahre viele Stunden damit verbracht habe, sie zu verstehen, zu reparieren, zu entwirren. und verschiedene Codebits erweitern.

Bearbeiten: Ich frage nicht nach homogenen Zirkelverweisen, wie in einer doppelt verknüpften Liste oder einem Zeiger auf ein übergeordnetes Element. Diese Frage stellt sich wirklich in Bezug auf Zirkelverweise mit "größerem Geltungsbereich", wie beispielsweise der Aufruf von libB durch libA, der libA zurückruft. Ersetzen Sie 'lib' durch 'module', wenn Sie möchten. Vielen Dank für alle bisherigen Antworten!


Bezieht sich der Zirkelverweis auf Bibliotheken und Header-Dateien? In einem Workflow verarbeitet der neue ProjectB-Code eine Datei, die aus dem alten ProjectA-Code ausgegeben wird. Diese Ausgabe von ProjectA ist eine neue Anforderung, die von ProjectB gesteuert wird. ProjectB verfügt über einen Code, mit dem sich generisch bestimmen lässt, welche Felder wohin gehen usw. Vorgänger-ProjectA könnte Code in neuem ProjectB wiederverwenden , und ProjectB wäre dumm, Dienstprogrammcode in Vorgänger-ProjectA nicht wiederzuverwenden (z. B .: Zeichensatzerkennung und -transcodierung, Datensatzanalyse, Datenvalidierung und -transformation usw.).
Luv2code

1
@ Luv2code Es wird nur dumm, wenn Sie den Code zwischen Projekten ausschneiden und einfügen oder wenn möglicherweise beide Projekte im selben Code kompilieren und verknüpfen. Wenn sie Ressourcen wie diese gemeinsam nutzen, legen Sie sie in einer Bibliothek ab.
Dash-Tom-Bang

Antworten:


220

Mit Zirkelverweisen stimmt vieles nicht:

  • Zirkuläre Klassenreferenzen erzeugen eine hohe Kopplung . beide Klassen müssen jedes Mal neu kompiliert werden , entweder von ihnen geändert wird .

  • Kreisförmige Anordnung Referenzen verhindern statische Linken , weil B auf A hängt aber A kann nicht montiert werden , bis B abgeschlossen ist.

  • Zirkuläre Objektreferenzen können naive rekursive Algorithmen (wie Serializer, Besucher und Pretty-Printers) mit Stapelüberläufen zum Absturz bringen . Die fortschrittlicheren Algorithmen verfügen über eine Zykluserkennung und schlagen lediglich mit einer aussagekräftigeren Ausnahme / Fehlermeldung fehl.

  • Zirkuläre Objektreferenzen machen auch die Abhängigkeitsinjektion unmöglich , wodurch die Testbarkeit Ihres Systems erheblich verringert wird.

  • Objekte mit einer sehr großen Anzahl von kreisförmigen Referenzen sind oft Gott Objekte . Auch wenn dies nicht der Fall ist, tendieren sie dazu, zu Spaghetti Code zu führen .

  • Zirkuläre Entitätsreferenzen (insbesondere in Datenbanken, aber auch in Domänenmodellen) verhindern die Verwendung von Einschränkungen für die Nichtnullierbarkeit , die möglicherweise zu einer Beschädigung der Daten oder zumindest zu Inkonsistenzen führen.

  • Zirkelverweise im Allgemeinen sind einfach verwirrend und erhöhen die kognitive Belastung drastisch, wenn versucht wird, die Funktionsweise eines Programms zu verstehen.

Denken Sie bitte an die Kinder. Vermeiden Sie Zirkelverweise, wann immer Sie können.


32
Ich schätze besonders den letzten Punkt: "Kognitive Belastung" ist etwas, das mir sehr bewusst ist, für das ich jedoch nie einen prägnanten Begriff hatte.
Dash-Tom-Bang

6
Gute Antwort. Es wäre besser, wenn Sie etwas über das Testen sagen würden. Wenn die Module A und B voneinander abhängig sind, müssen sie zusammen getestet werden. Dies bedeutet, dass es sich nicht wirklich um separate Module handelt. zusammen sind sie ein kaputtes Modul.
Kevin Cline

5
Die Abhängigkeitsinjektion ist mit Zirkelbezügen auch mit automatischer DI nicht unmöglich. Man muss nur eine Eigenschaft injizieren und nicht einen Konstruktorparameter.
BlueRaja - Danny Pflughoeft

3
@ BlueRaja-DannyPflughoeft: Ich halte das für ein Anti-Pattern, wie viele andere Praktiker von DI, weil (a) nicht klar ist, dass eine Eigenschaft tatsächlich eine Abhängigkeit ist, und (b) das Objekt nicht einfach "injiziert" werden kann Behalte den Überblick über seine eigenen Invarianten. Schlimmer noch, viele der ausgefeiltesten / beliebtesten Frameworks wie Castle Windsor können keine nützlichen Fehlermeldungen ausgeben, wenn eine Abhängigkeit nicht gelöst werden kann. Sie erhalten eine nervige Nullreferenz anstatt einer detaillierten Erklärung, welche Abhängigkeit in welchem ​​Konstruktor nicht aufgelöst werden konnte. Nur weil Sie können , heißt das nicht, dass Sie sollten .
Aaronaught

3
Ich habe nicht behauptet, es sei eine gute Praxis, sondern nur darauf hingewiesen, dass dies nicht unmöglich ist, wie in der Antwort behauptet.
BlueRaja - Danny Pflughoeft

22

Eine kreisförmige Referenz ist doppelt so groß wie die Kopplung einer nicht kreisförmigen Referenz.

Wenn Foo etwas über Bar weiß und Bar etwas über Foo weiß, müssen Sie zwei Dinge ändern (wenn die Anforderung kommt, dass Foos und Bars nichts mehr voneinander wissen müssen). Wenn Foo etwas über Bar weiß, aber eine Bar nichts über Foo weiß, können Sie Foo ändern, ohne Bar zu berühren.

Zyklische Verweise können auch Bootstrapping-Probleme verursachen, zumindest in Umgebungen mit langer Lebensdauer (bereitgestellte Dienste, abbildbasierte Entwicklungsumgebungen), in denen Foo davon abhängt, dass Bar arbeitet, um geladen zu werden, aber auch davon, dass Foo arbeitet, um zu laden Belastung.


17

Wenn Sie zwei Codebits zusammenbinden, haben Sie effektiv ein großes Codestück. Die Schwierigkeit, ein Stück Code zu pflegen, ist mindestens das Quadrat seiner Größe und möglicherweise höher.

Die Leute betrachten oft die Komplexität einzelner Klassen (/ function / file / etc.) Und vergessen, dass man wirklich die Komplexität der kleinsten trennbaren (verkapselbaren) Einheit berücksichtigen sollte. Durch eine zirkuläre Abhängigkeit wird die Größe dieser Einheit möglicherweise unsichtbar erhöht (bis Sie versuchen, Datei 1 zu ändern und feststellen, dass auch Änderungen in den Dateien 2-127 erforderlich sind).


14

Sie können schlecht sein, nicht für sich, sondern als Indikator für ein mögliches schlechtes Design. Wenn Foo von Bar und Bar von Foo abhängt, ist es gerechtfertigt, zu hinterfragen, warum es zwei statt einer eindeutigen FooBar sind.


10

Hmm ... das hängt davon ab, was Sie unter Kreisabhängigkeit verstehen, denn es gibt tatsächlich einige Kreisabhängigkeiten, von denen ich denke, dass sie sehr vorteilhaft sind.

Stellen Sie sich ein XML-DOM vor - es ist sinnvoll, dass jeder Knoten einen Verweis auf seinen übergeordneten Knoten hat und dass jeder übergeordnete Knoten eine Liste seiner untergeordneten Knoten hat. Die Struktur ist logischerweise ein Baum, aber aus der Sicht eines Speicherbereinigungsalgorithmus oder ähnlichem ist die Struktur kreisförmig.


1
wäre das nicht ein baum
Conrad Frix

@Conrad: Ich nehme an, man könnte es sich als einen Baum vorstellen, ja. Warum?
Billy ONeal

1
Ich halte den Baum nicht für kreisförmig, da Sie die untergeordneten Elemente des Baums nach unten navigieren können und ihn beenden (unabhängig von der Referenz der übergeordneten Elemente). Es sei denn, ein Knoten hatte ein Kind, das auch ein Vorfahre war, was es in meinen Augen zu einer Grafik und nicht zu einem Baum macht.
Conrad Frix

5
Ein Zirkelverweis wäre, wenn eines der untergeordneten Elemente eines Knotens zu einem Vorfahren zurückgeschleift wäre.
Matt Olenik

Dies ist eigentlich keine zirkuläre Abhängigkeit (zumindest nicht in einer Weise, die Probleme verursacht). Stellen Sie sich zum Beispiel vor, das Nodeist eine Klasse, auf die sich Nodedie Kinder in sich beziehen . Da die Klasse nur auf sich selbst verweist, ist sie vollständig in sich geschlossen und an nichts anderes gekoppelt. --- Mit diesem Argument könnte man argumentieren, dass eine rekursive Funktion eine zirkuläre Abhängigkeit ist. Es ist (am Stück), aber nicht schlecht.
Bis

9

Ist wie das Huhn oder das Ei Problem.

Es gibt viele Fälle, in denen ein zirkulärer Verweis unvermeidlich und nützlich ist, aber zum Beispiel im folgenden Fall nicht funktioniert:

Projekt A hängt von Projekt B ab und B hängt von A ab. A muss kompiliert werden, um in B verwendet zu werden, wo B vor A kompiliert werden muss, wo B vor A kompiliert werden muss, wo ...


6

Obwohl ich den meisten Kommentaren hier zustimme, möchte ich einen Sonderfall für den Zirkelverweis "Eltern" / "Kind" anführen.

Eine Klasse muss häufig etwas über ihre übergeordnete oder besitzende Klasse wissen, z. B. das Standardverhalten, den Namen der Datei, aus der die Daten stammen, die SQL-Anweisung, die die Spalte ausgewählt hat, oder den Speicherort einer Protokolldatei usw.

Sie können dies ohne einen Zirkelverweis tun, indem Sie eine enthaltende Klasse haben, so dass das, was zuvor "übergeordnet" war, jetzt ein Geschwister ist, aber es ist nicht immer möglich, vorhandenen Code neu zu faktorisieren, um dies zu tun.

Die andere Alternative besteht darin, alle Daten, die ein Kind benötigt, in seinem Konstruktor zu übergeben, was schlichtweg schrecklich ist.


In einem ähnlichen Zusammenhang gibt es zwei häufige Gründe, aus denen X möglicherweise einen Verweis auf Y hat: X möchte Y möglicherweise bitten, Dinge für X zu tun, oder Y erwartet, dass X für Y Dinge für Y tut. Wenn die einzigen Verweise auf Y zum Zweck anderer Objekte existieren, die Dinge in Ys Namen tun möchten, sollten die Inhaber solcher Verweise darauf hingewiesen werden, dass Ys Dienste nicht mehr benötigt werden und dass sie ihre Verweise auf Y um aufgeben sollten ihre Bequemlichkeit.
Supercat

5

In Bezug auf die Datenbank machen Zirkelverweise mit korrekten PK / FK-Beziehungen das Einfügen oder Löschen von Daten unmöglich. Wenn Sie nicht aus Tabelle a löschen können, es sei denn, der Datensatz ist aus Tabelle b entfernt und Sie können nicht aus Tabelle b löschen, es sei denn, der Datensatz ist aus Tabelle A entfernt, können Sie nicht löschen. Gleiches gilt für Einsätze. Aus diesem Grund können Sie in vielen Datenbanken keine kaskadierenden Aktualisierungen oder Löschvorgänge einrichten, wenn ein Zirkelverweis vorhanden ist, da dies zu einem bestimmten Zeitpunkt nicht mehr möglich ist. Ja, Sie können diese Art von Beziehungen einrichten, ohne dass die PK / Fk offiziell deklariert wird, aber dann werden Sie (nach meiner Erfahrung zu 100%) Probleme mit der Datenintegrität haben. Das ist nur schlechtes Design.


4

Ich werde diese Frage vom Standpunkt der Modellierung aus betrachten.

Solange Sie keine Beziehungen hinzufügen, die nicht tatsächlich vorhanden sind, sind Sie in Sicherheit. Wenn Sie sie hinzufügen, erhalten Sie eine geringere Datenintegrität (da Redundanz besteht) und engeren Code.

Das Besondere an den Zirkelverweisen ist, dass ich keinen Fall gesehen habe, in dem sie tatsächlich benötigt würden, außer einer Selbstreferenz. Wenn Sie Bäume oder Diagramme modellieren, benötigen Sie das und es ist vollkommen in Ordnung, da die Selbstreferenz aus Sicht der Codequalität harmlos ist (keine Abhängigkeit hinzugefügt).

Ich glaube, dass Sie im Moment, in dem Sie beginnen, eine Nicht-Selbstreferenz zu benötigen, sofort fragen sollten, ob Sie sie nicht als Grafik modellieren können (reduzieren Sie die mehreren Entitäten zu einem - Knoten). Vielleicht gibt es einen Fall dazwischen, in dem Sie einen Zirkelverweis erstellen, aber es ist nicht angebracht, ihn als Diagramm zu modellieren, aber ich bezweifle dies sehr.

Es besteht die Gefahr, dass die Menschen glauben, dass sie einen Zirkelverweis benötigen, dies jedoch nicht. Der häufigste Fall ist der "Einer von Vielen". Sie haben beispielsweise einen Kunden mit mehreren Adressen, von denen eine als primäre Adresse gekennzeichnet werden soll. Es ist sehr verlockend, diese Situation als zwei getrennte Beziehungen has_address und is_primary_address_of zu modellieren, aber es ist nicht korrekt. Der Grund dafür ist, dass die primäre Adresse keine separate Beziehung zwischen Benutzern und Adressen ist, sondern ein Attribut der Beziehung Adresse hat. Warum ist das so? Weil seine Domain auf die Adressen des Benutzers beschränkt ist und nicht auf alle Adressen, die es gibt. Sie wählen einen der Links aus und markieren ihn als den stärksten (primären).

(Wir werden jetzt über Datenbanken sprechen.) Viele Menschen entscheiden sich für die Zwei-Beziehungen-Lösung, weil sie unter "primär" einen eindeutigen Zeiger verstehen und ein Fremdschlüssel eine Art Zeiger ist. Also sollte der Fremdschlüssel das richtige sein, oder? Falsch. Fremdschlüssel stellen Beziehungen dar, aber "Primär" ist keine Beziehung. Es ist ein entarteter Fall einer Ordnung, bei der vor allem ein Element und der Rest nicht geordnet ist. Wenn Sie eine Gesamtreihenfolge modellieren müssten, würden Sie sie natürlich als Attribut einer Beziehung betrachten, da es im Grunde keine andere Wahl gibt. Aber in dem Moment, in dem Sie es degenerieren, gibt es eine Wahl und eine ziemlich schreckliche - etwas zu modellieren, das keine Beziehung als Beziehung darstellt. Hier kommt es also - Beziehungsredundanz, die sicherlich nicht zu unterschätzen ist.

Daher würde ich keinen Zirkelverweis zulassen, es sei denn, es ist absolut klar, dass er von dem stammt, was ich modelliere.

(Hinweis: Dies ist ein wenig voreingenommen gegenüber dem Datenbankdesign, aber ich wette, es ist auch für andere Bereiche angemessen anwendbar.)


2

Ich würde diese Frage mit einer anderen Frage beantworten:

In welcher Situation können Sie mir mitteilen, dass die Beibehaltung eines kreisförmigen Referenzmodells das beste Modell für das ist, was Sie erstellen möchten?

Meiner Erfahrung nach wird das beste Modell so gut wie nie Zirkelverweise enthalten, wie Sie es meinen. Abgesehen davon gibt es viele Modelle, bei denen Sie ständig Zirkelverweise verwenden. Das ist einfach extrem einfach. Übergeordnete -> untergeordnete Beziehungen, jedes Diagrammmodell usw., aber dies sind bekannte Modelle, und ich denke, Sie beziehen sich ganz auf etwas anderes.


1
Es KANN sein, dass eine zirkuläre verknüpfte Liste (einfach oder doppelt verknüpft) eine hervorragende Datenstruktur für die zentrale Ereigniswarteschlange für ein Programm darstellt, das "niemals anhalten" soll (die wichtigen N Dinge in die Warteschlange stellen, mit a) Setzen Sie das Flag "nicht löschen" und durchlaufen Sie die Warteschlange einfach, bis sie leer ist. Wenn neue Aufgaben (vorübergehend oder dauerhaft) erforderlich sind, kleben Sie sie an eine geeignete Stelle in die Warteschlange. Wenn Sie eine Gerade ohne das Flag "nicht löschen" bedienen , mach es dann nimm es aus der Warteschlange).
Vatine

1

Zirkelbezüge in Datenstrukturen sind manchmal die natürliche Art, ein Datenmodell auszudrücken. Coding-weise ist es definitiv nicht ideal und kann (zu einem gewissen Grad) durch Abhängigkeitsinjektion gelöst werden, wodurch das Problem vom Code auf Daten verlagert wird.


1

Ein Zirkelreferenzkonstrukt ist nicht nur vom Entwurfsstandpunkt aus problematisch, sondern auch vom Standpunkt des Fehlersuchens.

Berücksichtigen Sie die Möglichkeit eines Codefehlers. Sie haben in keiner Klasse die richtigen Fehlerbehebungen vorgenommen, entweder weil Sie Ihre Methoden noch nicht so weit entwickelt haben oder weil Sie faul sind. In beiden Fällen haben Sie keine Fehlermeldung, die Ihnen sagt, was passiert ist, und Sie müssen es debuggen. Als guter Programmdesigner wissen Sie, welche Methoden mit welchen Prozessen zusammenhängen, und können sie auf die Methoden einschränken, die für den Prozess relevant sind, der den Fehler verursacht hat.

Mit Zirkelverweisen haben sich Ihre Probleme jetzt verdoppelt. Da Ihre Prozesse eng miteinander verbunden sind, können Sie nicht wissen, welche Methode in welcher Klasse den Fehler verursacht haben könnte oder woher der Fehler kam, da eine Klasse von der anderen abhängig ist und von der anderen abhängig ist. Sie müssen nun beide Klassen gemeinsam testen, um herauszufinden, welcher wirklich für den Fehler verantwortlich ist.

Eine ordnungsgemäße Fehlerbehebung behebt dieses Problem natürlich nur, wenn Sie wissen, wann ein Fehler wahrscheinlich auftritt. Und wenn Sie allgemeine Fehlermeldungen verwenden, sind Sie immer noch nicht viel besser dran.


1

Einige Garbage Collectors haben Probleme beim Aufräumen, da jedes Objekt von einem anderen referenziert wird.

BEARBEITEN: Wie in den Kommentaren unten angegeben, gilt dies nur für einen äußerst naiven Versuch, einen Müllsammler zu finden, dem Sie in der Praxis niemals begegnen würden.


11
Hmm .. jeder Müllsammler, der dadurch ausfällt, ist kein echter Müllsammler.
Billy ONeal

11
Ich kenne keinen modernen Müllsammler, der Probleme mit Zirkelverweisen hätte. Zirkuläre Referenzen sind ein Problem, wenn Sie Referenzzählungen verwenden, aber die meisten Garbage Collectors einen Tracing-Stil verwenden (wobei Sie mit der Liste der bekannten Referenzen beginnen und diesen folgen, um alle anderen zu finden und alles andere zu sammeln).
Dean Harding

4
Siehe sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf , in dem die Nachteile für verschiedene Arten von Müllsammlern erklärt werden. haben Sie Probleme mit kreisförmigen Strukturen (wenn sich kreisförmige Objekte in verschiedenen Generationen befinden). Wenn Sie Referenzzählungen durchführen und mit der Behebung des Kreisreferenzproblems beginnen, führen Sie am Ende die langen Pausenzeiten ein, die für Mark und Sweep charakteristisch sind.
Ken Bloom

Wenn ein Garbage Collector Foo ansah und seinen Speicher freigab, der in diesem Beispiel auf Bar verweist, sollte er das Entfernen von Bar behandeln. An diesem Punkt ist es daher nicht erforderlich, dass der Müllsammler die Leiste entfernt, da dies bereits geschehen ist. Oder umgekehrt, wenn es Bar entfernt, welche Referenzen Foo soll es auch Foo entfernen, und es wird daher nicht nötig sein, Foo zu entfernen, weil es dies getan hat, als es Bar entfernt hat? Bitte korrigieren Sie mich, wenn ich falsch liege.
Chris

1
In Objective-C sorgen Zirkelverweise dafür, dass der Referenzzähler beim Loslassen nicht Null erreicht, was den Garbage Collector auslöst.
DexterW

-2

Meiner Meinung nach erleichtern uneingeschränkte Referenzen das Programmdesign, aber wir alle wissen, dass einige Programmiersprachen in bestimmten Zusammenhängen keine Unterstützung dafür bieten.

Sie erwähnten Referenzen zwischen Modulen oder Klassen. In diesem Fall ist es eine statische Sache, die vom Programmierer vordefiniert wurde, und es ist für den Programmierer eindeutig möglich, nach einer Struktur zu suchen, der es an Rundheit mangelt, obwohl sie möglicherweise nicht sauber zum Problem passt.

Das eigentliche Problem liegt in der Zirkularität von Laufzeitdatenstrukturen, bei denen einige Probleme nicht auf eine Weise definiert werden können, die die Zirkularität beseitigt. Letztendlich ist es jedoch das Problem, das diktieren sollte und das den Programmierer dazu zwingt, ein unnötiges Rätsel zu lösen.

Ich würde sagen, das ist ein Problem mit den Werkzeugen, kein Problem mit dem Prinzip.


Das Hinzufügen einer Ein-Satz-Meinung trägt nicht wesentlich zum Beitrag bei oder erklärt die Antwort. Könnten Sie das näher erläutern?

Nun, zwei Punkte, das Poster erwähnte tatsächlich Referenzen zwischen Modulen oder Klassen. In diesem Fall ist es eine statische Sache, die vom Programmierer vordefiniert wurde, und es ist für den Programmierer eindeutig möglich, nach einer Struktur zu suchen, der es an Rundheit mangelt, obwohl sie möglicherweise nicht sauber zum Problem passt. Das eigentliche Problem ist die Zirkularität in Laufzeitdatenstrukturen, bei denen einige Probleme nicht so definiert werden können, dass die Zirkularität beseitigt wird. Letztendlich ist es jedoch das Problem, das diktieren sollte und das den Programmierer dazu zwingt, ein unnötiges Rätsel zu lösen.
Josh S

Ich habe festgestellt, dass es einfacher ist, Ihr Programm zum Laufen zu bringen, aber im Allgemeinen ist es letztendlich schwieriger, die Software zu warten, da Sie feststellen, dass triviale Änderungen Kaskadeneffekte haben. A ruft zu B auf, wodurch A zurückgerufen wird, wodurch B zurückgerufen wird ... Ich habe festgestellt, dass es schwierig ist, die Auswirkungen derartiger Änderungen wirklich zu verstehen, insbesondere wenn A und B polymorph sind.
Dash-Tom-Bang
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.