Cosa c'è che non va nei riferimenti circolari?


160

Oggi sono stato coinvolto in una discussione di programmazione in cui ho fatto alcune affermazioni che in sostanza ipotizzavano assiomaticamente che i riferimenti circolari (tra moduli, classi, qualunque cosa) siano generalmente cattivi. Una volta che ho superato il mio passo, il mio collega ha chiesto, "cosa c'è che non va nei riferimenti circolari?"

Ho delle forti sensazioni al riguardo, ma è difficile per me verbalizzare in modo conciso e concreto. Qualsiasi spiegazione che mi viene in mente tende a fare affidamento su altri elementi che anch'io considero assiomi ("non posso usare in isolamento, quindi non posso testare", "comportamento sconosciuto / indefinito quando lo stato muta negli oggetti partecipanti", ecc. .), ma mi piacerebbe sentire una ragione concisa per cui i riferimenti circolari sono cattivi che non fanno i tipi di salti di fede che il mio cervello fa, avendo trascorso molte ore nel corso degli anni a districarli per capire, sistemare, ed estendere vari bit di codice.

Modifica: non sto chiedendo riferimenti circolari omogenei, come quelli in un elenco doppiamente collegato o da puntatore a genitore. Questa domanda si sta davvero ponendo domande su riferimenti circolari di "portata maggiore", come libA che chiama libB che richiama a libA. Sostituisci 'modulo' con 'lib' se vuoi. Grazie per tutte le risposte finora!


Il riferimento circolare riguarda le librerie e i file di intestazione? In un flusso di lavoro, il nuovo codice ProjectB elaborerà un file generato dal codice ProjectA legacy. L'output di ProjectA è un nuovo requisito guidato da ProjectB; ProjectB ha un codice che facilita la determinazione generica di quali campi vanno dove, ecc. Il punto, ProjectA legacy potrebbe riutilizzare il codice nel nuovo ProjectB, e ProjectB sarebbe sciocco non riutilizzare il codice utility nel ProjectA legacy (ad esempio: rilevamento e transcodifica di set di caratteri, analisi dei record, convalida e trasformazione dei dati, ecc.).
Luv2code

1
@ Luv2code Diventa sciocco solo se si taglia e incolla il codice tra progetti o eventualmente quando entrambi i progetti vengono compilati e collegati nello stesso codice. Se condividono risorse come questa, inseriscile in una libreria.
dash-tom-bang,

Risposte:


220

Ci sono molte cose sbagliate nei riferimenti circolari:

  • I riferimenti di classe circolari creano un elevato accoppiamento ; entrambe le classi devono essere ricompilate ogni volta che una di esse viene cambiata.

  • I riferimenti circolari dell'assieme impediscono il collegamento statico , poiché B dipende da A ma A non può essere assemblato fino al completamento di B.

  • I riferimenti ad oggetti circolari possono causare il crash di algoritmi ricorsivi ingenui (come serializzatori, visitatori e stampanti graziose) con overflow dello stack. Gli algoritmi più avanzati avranno il rilevamento del ciclo e falliranno semplicemente con un messaggio di errore / eccezione più descrittivo.

  • Anche i riferimenti ad oggetti circolari rendono impossibile l'iniezione di dipendenza , riducendo in modo significativo la testabilità del sistema.

  • Gli oggetti con un numero molto elevato di riferimenti circolari sono spesso oggetti God . Anche se non lo sono, hanno la tendenza a portare a Spaghetti Code .

  • I riferimenti alle entità circolari (specialmente nei database, ma anche nei modelli di dominio) impediscono l'uso di vincoli di non nullità , che possono eventualmente portare alla corruzione dei dati o almeno all'incoerenza.

  • I riferimenti circolari in generale sono semplicemente fonte di confusione e aumentano drasticamente il carico cognitivo quando si cerca di capire come funziona un programma.

Per favore, pensa ai bambini; evita riferimenti circolari ogni volta che puoi.


32
Apprezzo in particolare l'ultimo punto, "carico cognitivo" è qualcosa di cui sono molto consapevole ma non ho mai avuto un grande termine conciso per questo.
dash-tom-bang,

6
Buona risposta. Sarebbe meglio se dicessi qualcosa sui test. Se i moduli A e B sono reciprocamente dipendenti, devono essere testati insieme. Ciò significa che non sono moduli realmente separati; insieme sono un modulo rotto.
Kevin Cline,

5
L'iniezione di dipendenza non è impossibile con riferimenti circolari, anche con DI automatico. Uno dovrà solo essere iniettato con una proprietà piuttosto che come parametro del costruttore.
BlueRaja - Danny Pflughoeft

3
@ BlueRaja-DannyPflughoeft: ritengo che un anti-pattern, come fanno molti altri praticanti di DI, perché (a) non è chiaro che una proprietà sia effettivamente una dipendenza, e (b) l'oggetto che viene "iniettato" non può facilmente tenere traccia dei propri invarianti. Peggio ancora, molti dei framework più sofisticati / popolari come Castle Windsor non possono fornire utili messaggi di errore se non è possibile risolvere una dipendenza; si finisce con un fastidioso riferimento null invece di una spiegazione dettagliata di quale dipendenza in quale costruttore non possa essere risolto. Solo perché puoi , non significa che dovresti .
Aaronaught,

3
Non stavo sostenendo che fosse una buona pratica, stavo solo sottolineando che non è impossibile, come affermato nella risposta.
BlueRaja - Danny Pflughoeft il

22

Un riferimento circolare è il doppio dell'accoppiamento di un riferimento non circolare.

Se Foo conosce Bar e Bar sa Foo, hai due cose che devono essere cambiate (quando viene richiesto che Foos e Bar non devono più conoscersi). Se Foo conosce Bar, ma un Bar non conosce Foo, puoi cambiare Foo senza toccare Bar.

I riferimenti ciclici possono anche causare problemi di bootstrap, almeno in ambienti che durano a lungo (servizi distribuiti, ambienti di sviluppo basati su immagini), in cui Foo dipende dal funzionamento di Bar per caricare, ma Bar dipende anche dal funzionamento di Foo per caricare.


17

Quando si collegano due bit di codice, si ottiene effettivamente un grosso pezzo di codice. La difficoltà di mantenere un po 'di codice è almeno il quadrato delle sue dimensioni, e possibilmente maggiore.

Le persone spesso guardano alla complessità della singola classe (/ funzione / file / ecc.) E dimenticano che dovresti davvero considerare la complessità della più piccola unità separabile (incapsulabile). Avere una dipendenza circolare aumenta le dimensioni di quell'unità, possibilmente invisibilmente (fino a quando non inizi a provare a cambiare il file 1 e ti rendi conto che richiede anche modifiche nei file 2-127).


14

Potrebbero essere cattivi non da soli ma come indicatore di un possibile cattivo design. Se Foo dipende da Bar e Bar dipende da Foo, è giustificato chiedersi perché siano due invece di un FooBar unico.


10

Hmm ... dipende da cosa intendi per dipendenza circolare, perché in realtà ci sono alcune dipendenze circolari che penso siano molto utili.

Prendi in considerazione un DOM XML: ha senso che ogni nodo abbia un riferimento al proprio genitore e che ogni genitore abbia un elenco dei suoi figli. La struttura è logicamente un albero, ma dal punto di vista di un algoritmo di garbage collection o simile la struttura è circolare.


1
non sarebbe un albero?
Conrad Frix,

@Conrad: suppongo che potrebbe essere pensato come un albero, sì. Perché?
Billy ONeal,

1
Non penso che l'albero sia circolare perché puoi navigare lungo i suoi figli e terminerà (indipendentemente dal riferimento principale). A meno che un nodo non avesse un figlio che era anche un antenato che nella mia mente lo rende un grafico e non un albero.
Conrad Frix,

5
Un riferimento circolare sarebbe se uno dei figli di un nodo tornasse indietro a un antenato.
Matt Olenik,

Questa non è in realtà una dipendenza circolare (almeno non in un modo che causi problemi). Ad esempio, immagina che Nodesia una classe, che ha altri riferimenti a Nodeper i bambini dentro di sé. Poiché fa solo riferimento a se stesso, la classe è completamente autonoma e non è accoppiata a nient'altro. --- Con questo argomento, potresti sostenere che una funzione ricorsiva è una dipendenza circolare. Si tratta (a un tratto), ma non in un brutto modo.
byxor,

9

È come il problema del pollo o dell'uovo .

Esistono molti casi in cui i riferimenti circolari sono inevitabili e utili ma, ad esempio, nel seguente caso non funzionano:

Il progetto A dipende dal progetto B e B dipende da A. A deve essere compilato per essere utilizzato in B che richiede che B sia compilato prima di A che richiede che B sia compilato prima di A che ...


6

Mentre sono d'accordo con la maggior parte dei commenti qui, vorrei presentare un caso speciale per il riferimento circolare "genitore" / "figlio".

Una classe ha spesso bisogno di sapere qualcosa sul suo genitore o sulla sua classe proprietaria, forse sul comportamento predefinito, sul nome del file da cui provengono i dati, sull'istruzione sql che ha selezionato la colonna o sulla posizione di un file di registro ecc.

Puoi farlo senza un riferimento circolare avendo una classe contenitiva in modo che quello che prima era il "genitore" sia ora un fratello, ma non è sempre possibile ricodificare il codice esistente per farlo.

L'altra alternativa è passare tutti i dati di cui un bambino potrebbe aver bisogno nel suo costruttore, che alla fine è semplicemente orribile.


In una nota correlata, ci sono due motivi comuni per cui X potrebbe contenere un riferimento a Y: X potrebbe voler chiedere a Y di fare cose per conto di X, oppure Y potrebbe aspettarsi che X faccia cose a Y, per conto di Y. Se i soli riferimenti che esistono a Y sono per lo scopo di altri oggetti che vogliono fare cose per conto di Y, allora ai titolari di tali riferimenti dovrebbe essere detto che i servizi di Y non sono più necessari e che dovrebbero abbandonare i loro riferimenti a Y su la loro convenienza.
supercat,

5

In termini di database, i riferimenti circolari con relazioni PK / FK appropriate rendono impossibile l'inserimento o l'eliminazione di dati. Se non è possibile eliminare dalla tabella a se il record non è passato dalla tabella b e non è possibile eliminare dalla tabella b a meno che il record non sia passato dalla tabella A, non è possibile eliminare. Lo stesso con gli inserti. questo è il motivo per cui molti database non consentono di impostare aggiornamenti a cascata o eliminazioni se è presente un riferimento circolare perché a un certo punto non diventa possibile. Sì, puoi stabilire questo tipo di relazioni senza che il PK / Fk venga dichiarato formalmente, ma allora (il 100% delle volte nella mia esperienza) avrà problemi di integrità dei dati. Questo è solo un cattivo design.


4

Prenderò questa domanda dal punto di vista della modellazione.

Fintanto che non aggiungi relazioni che in realtà non ci sono, sei al sicuro. Se li aggiungi, otterrai meno integrità nei dati (causa ridondanza) e codice più strettamente associato.

La cosa con i riferimenti circolari in particolare è che non ho visto un caso in cui sarebbero stati effettivamente necessari tranne un riferimento personale. Se si modellano alberi o grafici, è necessario ed è perfettamente a posto perché l'autoreferenzialità è innocua dal punto di vista della qualità del codice (nessuna dipendenza aggiunta).

Credo che nel momento in cui inizi a aver bisogno di un non-auto-riferimento, dovresti immediatamente chiedere se non puoi modellarlo come un grafico (comprimere le entità multiple in uno - nodo). Forse c'è un caso tra cui si fa un riferimento circolare ma modellarlo come grafico non è appropriato ma ne dubito fortemente.

Esiste il pericolo che le persone pensino di aver bisogno di un riferimento circolare, ma in realtà non lo fanno. Il caso più comune è "Il caso uno dei tanti". Ad esempio, hai un cliente con più indirizzi da cui uno dovrebbe essere contrassegnato come indirizzo principale. È molto allettante modellare questa situazione come due relazioni separate has_address e is_primary_address_of ma non è corretto. Il motivo è che essere l'indirizzo principale non è una relazione separata tra utenti e indirizzi ma è invece un attributo della relazione con indirizzo. Perché? Perché il suo dominio è limitato agli indirizzi dell'utente e non a tutti gli indirizzi presenti. Scegli uno dei link e lo contrassegni come il più forte (primario).

(Adesso parliamo di database) Molte persone optano per la soluzione a due relazioni perché comprendono "primario" come un puntatore univoco e una chiave esterna è una specie di puntatore. Quindi la chiave esterna dovrebbe essere la cosa da usare, giusto? Sbagliato. Le chiavi esterne rappresentano relazioni ma "primaria" non è una relazione. È un caso degenerato di un ordinamento in cui un elemento è soprattutto e il resto non è ordinato. Se avessi bisogno di modellare un ordine totale, ovviamente lo considereresti come l'attributo di una relazione perché sostanzialmente non c'è altra scelta. Ma nel momento in cui la degeneri, c'è una scelta piuttosto orribile: modellare qualcosa che non è una relazione come relazione. Quindi ecco che arriva la ridondanza delle relazioni che non è certo qualcosa da sottovalutare.

Quindi, non permetterei che si verifichi un riferimento circolare a meno che non sia assolutamente chiaro che proviene dalla cosa che sto modellando.

(nota: questo è leggermente distorto per la progettazione del database, ma scommetto che è abbastanza applicabile anche ad altre aree)


2

Risponderei a questa domanda con un'altra domanda:

Quale situazione mi puoi dare se mantenere un modello di riferimento circolare è il modello migliore per quello che stai cercando di costruire?

Dalla mia esperienza, il modello migliore non coinvolgerà praticamente mai riferimenti circolari nel modo in cui penso che tu lo intenda. Detto questo, ci sono molti modelli in cui usi sempre riferimenti circolari, è semplicemente estremamente semplice. Genitore -> Relazioni figlio, qualsiasi modello di grafico, ecc., Ma questi sono modelli ben noti e penso che ti riferisca a qualcos'altro.


1
Può darsi che un elenco di collegamenti circolari (a collegamento singolo o doppio) sarebbe un'eccellente struttura di dati per la coda di eventi centrale per un programma che dovrebbe "non fermarsi mai" (attaccare le N cose importanti sulla coda, con un set di flag "non eliminare", quindi semplicemente attraversare la coda fino a quando non è vuoto; quando sono necessarie nuove attività (temporanee o permanenti), incollarle in un posto adatto sulla coda; ogni volta che si serve un anche senza il flag "non eliminare" , fallo quindi togliilo dalla coda).
Vatine,

1

I riferimenti circolari nelle strutture di dati sono talvolta il modo naturale di esprimere un modello di dati. Per quanto riguarda la codifica, non è sicuramente l'ideale e può essere (in una certa misura) risolto dall'iniezione di dipendenza, spingendo il problema dal codice ai dati.


1

Un costrutto di riferimento circolare è problematico, non solo dal punto di vista del design, ma anche dal punto di vista della cattura dell'errore.

Considera la possibilità di un errore del codice. Non hai inserito errori corretti nella rilevazione di entrambe le classi, o perché non hai ancora sviluppato i tuoi metodi finora o sei pigro. Ad ogni modo, non hai un messaggio di errore che ti dice cosa è emerso e devi eseguire il debug. Come buon programmatore di programmi, sai quali metodi sono correlati a quali processi, quindi puoi restringerlo a quei metodi rilevanti per il processo che ha causato l'errore.

Con riferimenti circolari, i tuoi problemi sono ora raddoppiati. Poiché i tuoi processi sono strettamente legati, non hai modo di sapere quale metodo in quale classe potrebbe aver causato l'errore o da dove provenga l'errore, perché una classe dipende dall'altra dipende dall'altra. Ora devi dedicare del tempo a testare entrambe le classi insieme per scoprire quale è realmente responsabile dell'errore.

Naturalmente, la corretta rilevazione degli errori risolve questo problema, ma solo se si sa quando è probabile che si verifichi un errore. E se stai usando messaggi di errore generici, non stai ancora molto meglio.


1

Alcuni garbage collector hanno problemi a ripulirli, perché a ogni oggetto fa riferimento un altro.

EDIT: Come notato dai commenti qui sotto, questo è vero solo per un tentativo estremamente ingenuo di un bidone della spazzatura, non uno che potresti mai incontrare in pratica.


11
Hmm .. qualsiasi spazzino inciampato da questo non è un vero spazzino.
Billy ONeal,

11
Non conosco nessun moderno garbage collector che avrebbe problemi con riferimenti circolari. I riferimenti circolari sono un problema se si utilizzano i conteggi dei riferimenti, ma la maggior parte dei raccoglitori di rifiuti sono in stile di tracciamento (in cui si inizia con l'elenco di riferimenti noti e li si segue per trovare tutti gli altri, raccogliendo tutto il resto).
Dean Harding,

4
Vedi sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf che spiega gli svantaggi di vari tipi di netturbini - se prendi il segno e spazzare e iniziare a ottimizzarlo per ridurre i lunghi tempi di pausa (es. Creazione di generazioni) , inizi ad avere problemi con le strutture circolari (quando gli oggetti circolari appartengono a generazioni diverse). Se si prendono i conteggi dei riferimenti e si inizia a risolvere il problema dei riferimenti circolari, si finisce per introdurre i lunghi tempi di pausa caratteristici di mark e sweep.
Ken Bloom,

Se un garbage collector guardava Foo e distribuiva la sua memoria che in questo esempio fa riferimento a Bar, dovrebbe gestire la rimozione di Bar. Quindi a questo punto non è necessario che Garbage Collector continui e rimuova la barra perché già funzionava. O viceversa, se rimuove la barra che fa riferimento a Foo, rimuoverà anche Foo e quindi non sarà necessario rimuovere Foo perché lo ha fatto quando ha rimosso la barra? Perfavore, correggimi se sbaglio.
Chris,

1
Nell'obiettivo-c, i riferimenti circolari lo rendono in modo tale che il conteggio dei riferimenti non raggiunga lo zero quando si rilascia, il che fa scattare il cestino della spazzatura.
DexterW

-2

A mio avviso, avere riferimenti illimitati semplifica la progettazione dei programmi, ma sappiamo tutti che alcuni linguaggi di programmazione non hanno il supporto per essi in alcuni contesti.

Hai citato riferimenti tra moduli o classi. In quel caso è una cosa statica, predefinita dal programmatore, ed è chiaramente possibile per il programmatore cercare una struttura priva di circolarità, anche se potrebbe non adattarsi perfettamente al problema.

Il vero problema si presenta nella circolarità nelle strutture di dati di runtime, in cui alcuni problemi in realtà non possono essere definiti in un modo che elimini la circolarità. Alla fine però - è il problema che dovrebbe dettare e richiedere qualcos'altro sta costringendo il programmatore a risolvere un puzzle non necessario.

Direi che è un problema con gli strumenti, non un problema con il principio.


L'aggiunta di un'opinione di una frase non contribuisce in modo significativo al post o spiega la risposta. Potresti approfondire questo?

Bene due punti, il poster in realtà menzionava riferimenti tra moduli o classi. In quel caso è una cosa statica, predefinita dal programmatore, ed è chiaramente possibile per il programmatore cercare una struttura priva di circolarità, anche se potrebbe non adattarsi perfettamente al problema. Il vero problema si presenta nella circolarità nelle strutture di dati di runtime, in cui alcuni problemi in realtà non possono essere definiti in un modo che elimini la circolarità. Alla fine però - è il problema che dovrebbe dettare e richiedere qualcos'altro sta costringendo il programmatore a risolvere un puzzle non necessario.
Josh S,

Ho scoperto che rende più semplice l'avvio e l'esecuzione del programma, ma che in generale alla fine rende più difficile la manutenzione del software poiché si scopre che cambiamenti insignificanti hanno effetti a cascata. A effettua chiamate in B che richiama A che effettua chiamate in B ... Ho trovato difficile comprendere veramente gli effetti dei cambiamenti di questa natura, specialmente quando A e B sono polimorfici.
dash-tom-bang
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.