Cosa c'è di sbagliato con i riferimenti circolari?

145

Sono stato coinvolto in una discussione di programmazione oggi dove ho fatto alcune affermazioni che fondamentalmente assumevano assiomaticamente che i riferimenti circolari (tra moduli, classi, qualunque cosa) sono generalmente cattivi. Una volta superato il mio tono, il mio collega mi ha chiesto "cosa c'è di sbagliato nei riferimenti circolari?"

Ho delle sensazioni forti su questo, ma è difficile per me verbalizzare in modo conciso e concreto. Qualsiasi spiegazione che io possa fornire tende ad affidarsi ad altri elementi che anch'io considero gli assiomi ("non può usare isolatamente, quindi non posso testare", "comportamento sconosciuto / indefinito come stato muta negli oggetti partecipanti", ecc. .), ma mi piacerebbe sentire una ragione concisa per cui i riferimenti circolari sono cattivi che non richiedono i salti di fede che il mio cervello fa, avendo passato molte ore nel corso degli anni a districarli per capire, correggere, ed estendi vari bit di codice.

Modifica: non sto chiedendo riferimenti circolari omogenei, come quelli in una lista doppiamente collegata o puntatore-a-genitore. Questa domanda è in realtà una domanda sui riferimenti circolari "scope più grandi", come libA che chiama libB che richiama alla libA. Sostituisci "modulo" per "lib" se vuoi. Grazie per tutte le risposte finora!

    
posta dash-tom-bang 14.10.2010 - 02:01
fonte

14 risposte

204

Ci sono molte grandi molte cose sbagliate con riferimenti circolari:

  • I riferimenti circolari classe creano un elevato accoppiamento ; entrambe le classi devono essere ricompilate ogni volta che o di esse è cambiato.

  • Circolari assembly riferimenti impedisci il collegamento statico , perché B dipende da A, ma A non può essere assemblato fino al completamento di B.

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

  • I riferimenti
  • Circolari rendono inoltre dipendenza da iniezione impossibile , riducendo significativamente la testabilità del tuo sistema.

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

  • I riferimenti
  • circolari dell'entità (soprattutto nei database, ma anche nei modelli di dominio) impediscono l'uso dei vincoli di non-nullità , che potrebbero portare alla corruzione dei dati o almeno incoerenza.

  • I riferimenti circolari in generale sono semplicemente confusi e aumentano drasticamente il carico cognitivo quando tenti di capire come funziona un programma.

Per favore, pensa ai bambini; evita riferimenti circolari quando puoi.

    
risposta data 14.10.2010 - 18:06
fonte
21

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

Se Foo conosce Bar, e Bar conosce Foo, hai due cose che devono essere cambiate (quando viene richiesto che Foos e Bars non debbano più sapere l'uno dell'altro). Se Foo conosce Bar, ma una Barra non sa di Foo, puoi cambiare Foo senza toccare Bar.

I riferimenti ciclici possono anche causare problemi di bootstrap, almeno in ambienti che durano a lungo (servizi implementati, ambienti di sviluppo basati su immagini), dove Foo dipende dalla barra che lavora per caricare, ma Bar dipende anche da Foo per caricare.

    
risposta data 14.10.2010 - 08:00
fonte
16

Quando si uniscono due bit di codice insieme, si ha effettivamente una grande porzione di codice. La difficoltà di mantenere un po 'di codice è almeno il quadrato delle sue dimensioni, e possibilmente superiore.

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

    
risposta data 14.10.2010 - 15:39
fonte
12

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

    
risposta data 14.10.2010 - 10:19
fonte
10

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

Considera un DOM XML - ha senso che ogni nodo abbia un riferimento al 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 raccolta dei dati inutili o simile la struttura è circolare.

    
risposta data 14.10.2010 - 02:39
fonte
9

È come il problema Chicken or the Egg .

Ci sono molti casi in cui il riferimento circolare è inevitabile e utile ma, ad esempio, nel caso seguente non funziona:

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

    
risposta data 14.10.2010 - 10:23
fonte
6

Pur condividendo la maggior parte dei commenti, vorrei invocare un caso speciale per il riferimento circolare "genitore" / "figlio".

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

Puoi farlo senza un riferimento circolare avendo una classe contenente in modo tale che quello che in precedenza era il "genitore" ora è un fratello, ma non è sempre possibile ridimensionare il codice esistente per farlo.

L'altra alternativa è quella di passare tutti i dati che un bambino potrebbe aver bisogno nel suo costruttore, che finiscono per essere semplicemente orribili.

    
risposta data 26.11.2013 - 08:33
fonte
5

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

    
risposta data 14.10.2010 - 16:34
fonte
3

Prenderò questa domanda dal punto di vista della modellazione.

Se non aggiungi nessuna relazione che non sia effettivamente lì, sei al sicuro. Se li aggiungi, ottieni meno integrità nei dati (perché esiste una ridondanza) e codice più strettamente accoppiato.

La cosa con i riferimenti circolari in particolare è che non ho visto un caso in cui sarebbero stati effettivamente necessari tranne uno - riferimento personale. Se modellate alberi o grafici, avete bisogno di questo ed è perfettamente a posto perché l'auto-riferimento è innocuo dal punto di vista della qualità del codice (nessuna dipendenza aggiunta).

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

C'è il pericolo che le persone pensino di aver bisogno di un riferimento circolare ma di fatto non lo fanno. Il caso più comune è "Il caso uno dei molti". Ad esempio, hai un cliente con più indirizzi tra cui uno deve essere contrassegnato come indirizzo principale. È molto allettante modellare questa situazione come due relazioni separate has_address e is_primary_address_of ma non è corretta. Il motivo è che essere l'indirizzo principale non è una relazione separata tra utenti e indirizzi, ma è un attributo della relazione ha un indirizzo . Perché? Perché il suo dominio è limitato agli indirizzi dell'utente e non a tutti gli indirizzi che ci sono. Scegli uno dei link e contrassegnalo come il più strong (primario).

(Adesso parliamo di database) Molte persone optano per la soluzione a due relazioni perché capiscono che "primario" è 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 le relazioni ma "primaria" non è una relazione. È un caso degenerato di un ordine in cui un elemento è soprattutto e il resto non è ordinato. Se avessi bisogno di modellare un ordinamento totale, lo considereresti come attributo di una relazione perché in fondo non esiste altra scelta. Ma al momento si degenera, c'è una scelta e piuttosto orribile - modellare qualcosa che non è una relazione come relazione. Quindi ecco che arriva - la ridondanza delle relazioni che non è certamente qualcosa da sottovalutare. Il requisito di unicità dovrebbe essere imposto in altro modo, ad esempio mediante indici parziali unici.

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

(nota: questo è leggermente sbilanciato rispetto al design del database, ma direi che è abbastanza applicabile anche ad altre aree)

    
risposta data 29.11.2013 - 04:02
fonte
2

Risponderei a questa domanda con un'altra domanda:

Quale situazione puoi darmi dove mantenere un modello di riferimento circolare è il miglior modello per ciò che stai cercando di costruire?

Dalla mia esperienza, il modello migliore praticamente non coinvolgerà mai riferimenti circolari nel modo in cui penso che tu intenda. Detto questo, ci sono molti modelli in cui si usano riferimenti circolari per tutto il tempo, è semplicemente estremamente semplice. Parent - > Relazioni tra bambini, qualsiasi modello di grafico, ecc., Ma questi sono modelli ben noti e penso che ti riferisci a qualcos'altro.

    
risposta data 14.10.2010 - 02:39
fonte
1

I riferimenti circolari nelle strutture dati sono a volte il modo naturale di esprimere un modello di dati. Dal punto di vista della codifica, non è assolutamente l'ideale e può essere (in parte) risolto dall'iniezione delle dipendenze, spingendo il problema dal codice ai dati.

    
risposta data 29.11.2013 - 11:34
fonte
1

Un costrutto circolare di riferimento è problematico, non solo dal punto di vista del design, ma anche da un punto di vista che cattura gli errori.

Considera la possibilità di un errore di codice. Non hai inserito correttamente l'errore di cattura in nessuna delle due classi, perché non hai ancora sviluppato i tuoi metodi fino ad ora, o sei pigro. In entrambi i casi, non hai un messaggio di errore per dirti cosa è accaduto ed è necessario eseguirne il debug. Come un buon programmatore di programmi, sai quali metodi sono correlati a quali processi, in modo da poterlo restringere a quei metodi rilevanti per il processo che ha causato l'errore.

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

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

    
risposta data 29.05.2015 - 16:23
fonte
1

Alcuni garbage collector hanno problemi a ripulirli, perché ogni oggetto viene referenziato da un altro.

EDIT: Come notato dai commenti qui sotto, questo è vero solo per un tentativo ingenuo estremamente in un garbage collector, non uno che avresti mai incontrato in pratica.

    
risposta data 14.10.2010 - 02:36
fonte
-2

Secondo me, avere riferimenti illimitati facilita la progettazione del programma, ma sappiamo tutti che alcuni linguaggi di programmazione non hanno il supporto per loro in alcuni contesti.

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

Il vero problema si presenta in circolarità nelle strutture di dati del tempo di esecuzione, in cui alcuni problemi in realtà non possono essere definiti in un modo che elimina la circolarità. Alla fine però - è il problema che dovrebbe dettare e che richiede qualcos'altro sta costringendo il programmatore a risolvere un enigma inutile.

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

    
risposta data 11.05.2014 - 03:23
fonte

Leggi altre domande sui tag