O que há de errado com referências circulares?

160

Hoje, participei de uma discussão de programação em que fiz algumas declarações que basicamente supunham axiomaticamente que referências circulares (entre módulos, classes, o que for) geralmente são ruins. Depois que terminei meu discurso, meu colega de trabalho perguntou: "o que há de errado nas referências circulares?"

Eu tenho sentimentos fortes sobre isso, mas é difícil para mim verbalizar concisa e concretamente. Qualquer explicação que eu possa apresentar tende a depender de outros itens que eu também considero axiomas ("não podem ser utilizados isoladamente, portanto não podem ser testados", "comportamento desconhecido / indefinido à medida que o estado sofre mutação nos objetos participantes", etc. .), mas eu adoraria ouvir uma razão concisa para o porquê de referências circulares serem ruins e não dar o tipo de salto de fé que meu próprio cérebro faz, depois de passar muitas horas ao longo dos anos desmembrando-as para entender, consertar, e estender vários bits de código.

Edit: Eu não estou perguntando sobre referências circulares homogêneas, como aquelas em uma lista duplamente vinculada ou ponteiro para o pai. Esta questão está realmente perguntando sobre referências circulares de "escopo maior", como libA chamando libB, que chama de volta para libA. Substitua 'module' por 'lib' se desejar. Obrigado por todas as respostas até agora!

traço-tom-bang
fonte
A referência circular pertence a bibliotecas e arquivos de cabeçalho? Em um fluxo de trabalho, o novo código do ProjectB processará um arquivo que é gerado pelo código legado do ProjectA. Essa saída do Projeto A é um novo requisito impulsionado pelo Projeto B; O ProjectB possui um código que facilita a determinação generosa de quais campos vão para onde, etc. A questão é que o ProjectA legado poderia reutilizar o código no novo ProjectB e o ProjectB seria tolo em não reutilizar o código do utilitário no ProjectA legado (por exemplo: detecção e transcodificação de conjuntos de caracteres, análise de registros, validação e transformação de dados, etc.).
Luv2code
1
@ Luv2code Torna-se tolo apenas se você recortar e colar o código entre projetos ou, possivelmente, quando ambos os projetos forem compilados e vinculados no mesmo código. Se eles estiverem compartilhando recursos como esse, coloque-os em uma biblioteca.
traço-tom-bang

Respostas:

220

muitas coisas erradas nas referências circulares:

  • As referências de classe circular criam alto acoplamento ; ambas as classes devem ser recompiladas toda vez que uma delas for alterada.

  • As referências de montagem circular impedem a vinculação estática , porque B depende de A, mas A não pode ser montado até que B seja concluído.

  • As referências circulares a objetos podem travar algoritmos recursivos ingênuos (como serializadores, visitantes e impressoras bonitas) com estouros de pilha. Os algoritmos mais avançados terão detecção de ciclo e apenas falharão com uma mensagem de exceção / erro mais descritiva.

  • As referências circulares a objetos também tornam impossível a injeção de dependência , reduzindo significativamente a testabilidade do seu sistema.

  • Objetos com um número muito grande de referências circulares geralmente são objetos de Deus . Mesmo se não estiverem, eles tendem a levar ao Código de Espaguete .

  • Referências de entidades circulares (especialmente em bancos de dados, mas também em modelos de domínio) impedem o uso de restrições de nulidade , que podem levar a corrupção de dados ou pelo menos inconsistência.

  • As referências circulares em geral são simplesmente confusas e aumentam drasticamente a carga cognitiva ao tentar entender como um programa funciona.

Por favor, pense nas crianças; evite referências circulares sempre que puder.

Aaronaught
fonte
32
Aprecio particularmente o último ponto: "carga cognitiva" é algo de que tenho muita consciência, mas nunca tive um termo conciso para isso.
Dash-tom-bang
6
Boa resposta. Seria melhor se você dissesse algo sobre o teste. Se os módulos A e B forem mutuamente dependentes, eles devem ser testados juntos. Isso significa que eles não são realmente módulos separados; juntos eles são um módulo quebrado.
Kevin cline
5
A injeção de dependência não é impossível com referências circulares, mesmo com DI automático. Apenas será necessário injetar uma propriedade em vez de um parâmetro construtor.
precisa saber é o seguinte
3
@ BlueRaja-DannyPflughoeft: Considero que um antipadrão, assim como muitos outros profissionais de DI, porque (a) não está claro que uma propriedade seja realmente uma dependência e (b) o objeto que está sendo "injetado" não pode ser facilmente mantenha o controle de seus próprios invariantes. Pior ainda, muitas das estruturas mais sofisticadas / populares, como Castle Windsor, não podem fornecer mensagens de erro úteis se uma dependência não puder ser resolvida; você acaba com uma referência nula irritante em vez de uma explicação detalhada de exatamente qual dependência em qual construtor não pôde ser resolvido. Só porque você pode , não significa que você deveria .
precisa saber é o seguinte
3
Eu não estava afirmando que é uma boa prática, apenas estava apontando que não é impossível, como reivindicado na resposta.
BlueRaja - Danny Pflughoeft
22

Uma referência circular é o dobro do acoplamento de uma referência não circular.

Se Foo souber sobre Bar, e Bar souber sobre Foo, você terá duas coisas que precisam ser alteradas (quando houver a exigência de que Foos e Bars não precisem mais se conhecer). Se Foo souber sobre Bar, mas um Bar não souber sobre Foo, você poderá alterá-lo sem tocar em Bar.

As referências cíclicas também podem causar problemas de inicialização, pelo menos em ambientes que duram muito tempo (serviços implantados, ambientes de desenvolvimento com base em imagens), onde Foo depende de Bar trabalhando para carregar, mas Bar também depende de Foo trabalhando para carga.

Frank Shearar
fonte
17

Quando você une dois bits de código, você efetivamente possui um grande pedaço de código. A dificuldade de manter um pouco de código é pelo menos o quadrado de seu tamanho e possivelmente maior.

As pessoas geralmente olham para a complexidade de classe única (/ function / file / etc.) E esquecem que você realmente deve considerar a complexidade da menor unidade separável (encapsulável). Ter uma dependência circular aumenta o tamanho dessa unidade, possivelmente invisível (até você começar a tentar alterar o arquivo 1 e perceber que isso também exige alterações nos arquivos 2-127).

Alex Feinman
fonte
14

Eles podem ser ruins não sozinhos, mas como um indicador de um possível design inadequado. Se Foo depende de Bar e Bar depende de Foo, justifica-se questionar por que eles são dois, em vez de um FooBar exclusivo.

mouviciel
fonte
10

Hmm ... isso depende do que você quer dizer com dependência circular, porque na verdade existem algumas dependências circulares que eu acho muito benéficas.

Considere um DOM XML - faz sentido que cada nó tenha uma referência ao pai e que todo pai tenha uma lista de seus filhos. A estrutura é logicamente uma árvore, mas do ponto de vista de um algoritmo de coleta de lixo ou similar, a estrutura é circular.

Billy ONeal
fonte
1
isso não seria uma árvore?
Conrad Frix
@Conrad: Suponho que possa ser pensado como uma árvore, sim. Por quê?
Billy ONeal
1
Eu não acho que a árvore seja circular porque você pode navegar por seus filhos e será encerrado (independentemente da referência dos pais). A menos que um nó tenha um filho que também seja um ancestral, o que, na minha opinião, faz dele um gráfico e não uma árvore.
Conrad Frix
5
Uma referência circular seria se um dos filhos de um nó retornasse a um ancestral.
Matt Olenik
Esta não é realmente uma dependência circular (pelo menos não de uma maneira que cause problemas). Por exemplo, imagine que Nodeé uma classe, que tem outras referências Nodepara as crianças dentro de si. Por se referir apenas a si mesma, a classe é completamente independente e não é acoplada a mais nada. --- Com esse argumento, você poderia argumentar que uma função recursiva é uma dependência circular. Ele é (em um trecho), mas não de uma maneira ruim.
Byxor
9

É como o problema da galinha ou do ovo .

Há muitos casos em que a referência circular é inevitável e útil, mas, por exemplo, no seguinte caso, não funciona:

O projeto A depende do projeto B e B depende de A. A precisa ser compilado para ser usado em B, o que exige que B seja compilado antes de A que requer que B seja compilado antes de A ...

Victor Hurdugaci
fonte
6

Embora eu concorde com a maioria dos comentários aqui, gostaria de defender um caso especial para a referência circular "pai" / "filho".

Uma classe geralmente precisa saber algo sobre sua classe principal ou proprietária, talvez o comportamento padrão, o nome do arquivo de origem dos dados, a instrução sql que selecionou a coluna ou a localização de um arquivo de log etc.

Você pode fazer isso sem uma referência circular, tendo uma classe que contenha, para que o que era anteriormente o "pai" agora seja um irmão, mas nem sempre é possível re-fatorar o código existente para fazer isso.

A outra alternativa é passar todos os dados que uma criança possa precisar em seu construtor, que acabam sendo simplesmente horríveis.

James Anderson
fonte
Em uma nota relacionada, há dois motivos comuns pelos quais X pode ter uma referência a Y: X pode pedir para Y fazer coisas em nome de X ou Y pode esperar que X faça coisas em Y, em nome de Y. Se as únicas referências que existem para Y são para o objetivo de outros objetos que desejam fazer coisas em nome de Y, os detentores dessas referências devem ser informados de que os serviços de Y não são mais necessários e que devem abandonar suas referências a Y em a conveniência deles.
Supercat #
5

Em termos de banco de dados, referências circulares com relacionamentos PK / FK adequados tornam impossível inserir ou excluir dados. Se você não pode excluir da tabela a, a menos que o registro tenha saído da tabela be você não pode excluir da tabela b, a menos que o registro tenha saído da tabela A, não será possível excluir. Mesmo com inserções. é por isso que muitos bancos de dados não permitem configurar atualizações ou exclusões em cascata, se houver uma referência circular, porque, em algum momento, isso não será possível. Sim, você pode configurar esse tipo de relacionamento sem que o PK / Fk seja formalmente declarado, mas você (100% do tempo na minha experiência) terá problemas de integridade de dados. Isso é apenas um design ruim.

HLGEM
fonte
4

Vou levar essa pergunta do ponto de vista da modelagem.

Contanto que você não adicione nenhum relacionamento que não esteja realmente lá, você estará seguro. Se você adicioná-los, obterá menos integridade nos dados (porque há uma redundância) e um código mais fortemente acoplado.

A coisa com as referências circulares especificamente é que eu não vi um caso em que elas seriam realmente necessárias, exceto uma referência própria. Se você modelar árvores ou gráficos, precisará disso e está perfeitamente certo, porque a auto-referência é inofensiva do ponto de vista da qualidade do código (sem dependência adicionada).

Acredito que, no momento em que você começa a precisar de uma referência que não seja auto, você deve perguntar imediatamente se não pode modelá-lo como um gráfico (recolher as várias entidades em um nó). Talvez exista um caso no qual você faça uma referência circular, mas modelá-la como gráfico não é apropriado, mas duvido muito disso.

Existe o perigo de as pessoas pensarem que precisam de uma referência circular, mas na verdade não precisam. O caso mais comum é "O-um-de-muitos". Por exemplo, você tem um cliente com vários endereços dos quais um deve ser marcado como o endereço principal. É muito tentador modelar essa situação como dois relacionamentos separados has_address e is_primary_address_of, mas não está correto. O motivo é que ser o endereço principal não é um relacionamento separado entre usuários e endereços, mas sim um atributo do relacionamento que possui endereço. Por que é que? Porque seu domínio é limitado aos endereços do usuário e não a todos os endereços existentes. Você escolhe um dos links e o marca como o mais forte (primário).

(Agora vamos falar sobre bancos de dados) Muitas pessoas optam pela solução de dois relacionamentos porque entendem "primário" como um ponteiro exclusivo e uma chave estrangeira é um tipo de ponteiro. Então, chave estrangeira deve ser a melhor opção, certo? Errado. Chaves estrangeiras representam relacionamentos, mas "primário" não é um relacionamento. É um caso degenerado de uma ordem em que um elemento está acima de tudo e o restante não está ordenado. Se você precisasse modelar um pedido total, certamente o consideraria como um atributo de relacionamento, porque basicamente não há outra opção. Mas no momento em que você o degenerou, há uma escolha bastante horrível - modelar algo que não é um relacionamento como um relacionamento. Então aqui vem - redundância de relacionamento que certamente não é algo a ser subestimado.

Portanto, eu não permitiria que uma referência circular ocorresse, a menos que seja absolutamente claro que ela vem da coisa que estou modelando.

(nota: isso é um pouco tendencioso para o design do banco de dados, mas aposto que também é aplicável a outras áreas)

clima
fonte
2

Eu responderia a essa pergunta com outra pergunta:

Que situação você pode me dar em que manter um modelo de referência circular é o melhor para o que você está tentando construir?

Pela minha experiência, o melhor modelo praticamente nunca envolverá referências circulares da maneira que eu acho que você quer dizer. Dito isto, existem muitos modelos em que você usa referências circulares o tempo todo, é apenas extremamente básico. Pai -> Relacionamentos filhos, qualquer modelo de gráfico, etc., mas esses são modelos bem conhecidos e acho que você está se referindo a algo completamente diferente.

Joseph
fonte
1
PODE ser que uma lista vinculada circular (com um ou dois links) seja uma excelente estrutura de dados para a fila de eventos central de um programa que "nunca pare" (cole as N coisas importantes na fila, com um conjunto de sinalizadores "não exclua" e, em seguida, basta atravessar a fila até ficar vazio; quando novas tarefas (transitórias ou permanentes) forem necessárias, cole-as em um local adequado na fila; sempre que você servir um sinalizador mesmo sem o sinal de "não excluir" , retire-o da fila).
Vatine 14/10/10
1

Às vezes, referências circulares em estruturas de dados são a maneira natural de expressar um modelo de dados. Em termos de codificação, definitivamente não é o ideal e pode ser (até certo ponto) resolvido por injeção de dependência, empurrando o problema do código para os dados.

Vatine
fonte
1

Uma construção de referência circular é problemática, não apenas do ponto de vista do design, mas também do ponto de vista de captura de erros.

Considere a possibilidade de uma falha de código. Você não colocou a captura adequada de erros em nenhuma das classes, porque ainda não desenvolveu seus métodos até agora ou é preguiçoso. De qualquer forma, você não tem uma mensagem de erro para informar o que aconteceu e é necessário depurá-la. Como um bom designer de programa, você sabe quais métodos estão relacionados a quais processos, para poder reduzi-lo aos métodos relevantes ao processo que causou o erro.

Com referências circulares, seus problemas agora dobraram. Como seus processos são fortemente vinculados, você não tem como saber qual método em qual classe pode ter causado o erro ou de onde o erro ocorreu, porque uma classe depende da outra e depende da outra. Agora você precisa gastar tempo testando as duas classes em conjunto para descobrir qual é realmente responsável pelo erro.

Obviamente, a captura adequada de erros resolve isso, mas somente se você souber quando é provável que ocorra um erro. E se você estiver usando mensagens de erro genéricas, ainda não está muito melhor.

Zibbobz
fonte
1

Alguns coletores de lixo têm problemas para limpá-los, porque cada objeto está sendo referenciado por outro.

EDIT: Como observado pelos comentários abaixo, isso é verdade apenas para uma tentativa extremamente ingênua de um coletor de lixo, não aquela que você encontraria na prática.

shmuelp
fonte
11
Hmm ... qualquer coletor de lixo que tropeçou com isso não é um verdadeiro coletor de lixo.
Billy ONeal
11
Não conheço nenhum coletor de lixo moderno que tenha problemas com referências circulares. As referências circulares são um problema se você estiver usando contagens de referência, mas a maioria dos coletores de lixo tem estilo de rastreamento (onde você começa com a lista de referências conhecidas e as segue para encontrar todas as outras, coletando todo o resto).
Dean Harding
4
Veja sct.ethz.ch/teaching/ws2005/semspecver/slides/takano.pdf, que explica as desvantagens de vários tipos de coletores de lixo - se marcar e varrer e começar a otimizá-lo para reduzir os longos tempos de pausa (por exemplo, geração de gerações) , você começa a ter problemas com estruturas circulares (quando objetos circulares estão em gerações diferentes). Se você faz as contagens de referência e começa a corrigir o problema de referência circular, acaba introduzindo os longos tempos de pausa característicos da marcação e da varredura.
Ken Bloom
Se um coletor de lixo olhar para Foo e desalocar sua memória, que neste exemplo faz referência a Bar, ele deve tratar da remoção de Bar. Portanto, neste ponto, não há necessidade de o coletor de lixo ir em frente e remover a barra, porque já o fez. Ou vice-versa, se remover Bar que faz referência a Foo, ele também removerá Foo e, portanto, não precisará remover o Foo porque o fez quando removeu Bar? Por favor corrija-me se eu estiver errado.
Chris
1
No objetivo-c, as referências circulares fazem com que a contagem de referências não atinja zero quando você libera, o que aciona o coletor de lixo.
DexterW
-2

Na minha opinião, ter referências irrestritas facilita o design do programa, mas todos sabemos que algumas linguagens de programação não têm suporte para elas em alguns contextos.

Você mencionou referências entre módulos ou classes. Nesse caso, é uma coisa estática, predefinida pelo programador, e é claramente possível para o programador procurar uma estrutura que carece de circularidade, embora possa não se encaixar perfeitamente no problema.

O problema real ocorre na circularidade nas estruturas de dados em tempo de execução, onde alguns problemas não podem ser definidos de uma maneira que se livra da circularidade. No final, porém - é o problema que deve ditar e exigir qualquer outra coisa está forçando o programador a resolver um quebra-cabeça desnecessário.

Eu diria que é um problema com as ferramentas, não um problema com o princípio.

Josh S
fonte
Adicionar uma opinião de uma frase não contribui significativamente para a postagem nem explica a resposta. Você poderia elaborar isso?
Bem, dois pontos, o pôster realmente mencionou referências entre módulos ou classes. Nesse caso, é uma coisa estática, predefinida pelo programador, e é claramente possível para o programador procurar uma estrutura que carece de circularidade, embora possa não se encaixar perfeitamente no problema. O problema real ocorre na circularidade nas estruturas de dados em tempo de execução, onde alguns problemas não podem ser definidos de uma maneira que se livra da circularidade. No final, porém - é o problema que deve ditar e exigir qualquer outra coisa está forçando o programador a resolver um quebra-cabeça desnecessário.
11134 Josh S
Descobri que isso facilita a execução do programa, mas que, em geral, torna mais difícil a manutenção do software, pois você acha que alterações triviais têm efeitos em cascata. A faz chamadas para B, que faz chamadas de volta para A, que faz chamadas de volta para B ... Descobri que é difícil entender verdadeiramente os efeitos de mudanças dessa natureza, especialmente quando A e B são polimórficos.
dash-tom-bang