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

145

Eu estava envolvido em uma discussão de programação hoje em que fiz algumas declarações que basicamente assumiam axiomaticamente que referências circulares (entre módulos, classes, qualquer que seja) são geralmente ruins. Depois que terminei meu discurso, meu colega de trabalho perguntou: "o que há de errado com referências circulares?"

Eu tenho strongs sentimentos sobre isso, mas é difícil para mim verbalizar de forma concisa e concreta. Qualquer explicação que eu possa apresentar tende a confiar em outros itens que eu também considero axiomas ("não posso usar isoladamente, por isso não posso testar", "comportamento desconhecido / indefinido como estado mutado nos objetos participantes", etc). .), mas eu adoraria ouvir uma razão concisa para o porquê de referências circulares serem ruins que não levam aos tipos de fé que meu próprio cérebro faz, tendo passado muitas horas ao longo dos anos desembaraçando-os para entender, consertar, e estender vários bits de código.

Editar: Não estou perguntando sobre referências circulares homogêneas, como aquelas em uma lista duplamente vinculada ou ponteiro para 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 você quiser. Obrigado por todas as respostas até agora!

    
por dash-tom-bang 14.10.2010 / 02:01
fonte

14 respostas

204

Há muitas coisas muitas erradas com referências circulares:

  • Referências de classe circular criam acoplamento alto; As classes both devem ser recompiladas sempre que qualquer delas for alterada.

  • Referências de montagens circulares impedem a vinculação estática , porque B depende de A, mas A não pode ser montado até que B esteja completo.

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

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

  • Objetos com um número muito grande de referências circulares são frequentemente Objetos divinos . Mesmo que não sejam, eles tendem a levar ao Código do espaguete .

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

  • 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.

    
por 14.10.2010 / 18:06
fonte
21

Uma referência circular é duas vezes o acoplamento de uma referência não circular.

Se Foo sabe sobre Bar, e Bar sabe sobre Foo, você tem duas coisas que precisam ser alteradas (quando a exigência é que Foos e Bares não devem mais saber um do outro). Se Foo sabe sobre Bar, mas um Bar não sabe sobre Foo, você pode trocar Foo sem tocar em Bar.

Referências cíclicas também podem causar problemas de bootstrapping, pelo menos em ambientes que duram muito tempo (serviços implementados, ambientes de desenvolvimento baseados em imagem), onde o Foo depende do trabalho do Bar para carregar, mas o Bar também depende do trabalho do Foo para carregar.

    
por 14.10.2010 / 08:00
fonte
16

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

As pessoas geralmente observam a complexidade de uma única classe (/ function / file / etc.) e esquecem que você deveria estar considerando a complexidade da menor unidade separável (encapsulável). Ter uma dependência circular aumenta o tamanho dessa unidade, possivelmente de forma invisível (até você começar a tentar alterar o arquivo 1 e perceber que isso também requer mudanças nos arquivos 2-127).

    
por 14.10.2010 / 15:39
fonte
12

Elas podem ser ruins, não por si mesmas, mas como um indicador de um possível mau planejamento. Se Foo depende de Bar e Bar depende de Foo, justifica-se questionar porque são dois em vez de uma FooBar única.

    
por 14.10.2010 / 10:19
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 que são muito benéficas.

Considere um DOM XML - faz sentido que cada nó tenha uma referência a seu pai e que cada 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.

    
por 14.10.2010 / 02:39
fonte
9

É como o problema Frango ou o ovo .

Existem muitos casos em que referências circulares são inevitáveis e úteis, mas, por exemplo, no caso a seguir, 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 requer que B seja compilado antes de A, o qual requer que B seja compilado antes de A, ...

    
por 14.10.2010 / 10:23
fonte
6

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

Uma classe geralmente precisa saber algo sobre sua classe pai ou proprietária, talvez o comportamento padrão, o nome do arquivo de onde vieram os dados, a instrução sql que selecionou a coluna ou a localização de um arquivo de log, etc. / p>

Você pode fazer isso sem uma referência circular tendo uma classe contendo, de modo que o que antes era o "pai" agora é um irmão, mas nem sempre é possível refatorar o código existente para fazer isso.

A outra alternativa é passar todos os dados que uma criança pode precisar em seu construtor, o que acaba sendo simplesmente horrível.

    
por 26.11.2013 / 08:33
fonte
5

Em termos de banco de dados, referências circulares com relacionamentos PK / FK apropriados tornam impossível inserir ou excluir dados. Se você não puder excluir da tabela a, a menos que o registro tenha saído da tabela be não possa ser excluído da tabela b, a menos que o registro tenha saído da tabela A, não será possível excluí-lo. O mesmo com inserções. É por isso que muitos bancos de dados não permitem que você configure atualizações ou exclusões em cascata se houver uma referência circular porque, em algum momento, isso não é 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 design ruim.

    
por 14.10.2010 / 16:34
fonte
3

Vou pegar essa questão do ponto de vista da modelagem.

Desde que você não adicione nenhum relacionamento que não esteja realmente presente, você estará seguro. Se você adicioná-los, você terá menos integridade nos dados (porque há uma redundância) e um código mais 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 auto-referência. Se você modelar árvores ou gráficos, você precisará disso e estará perfeitamente correto, pois 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çar a precisar de uma referência não-própria, imediatamente você deve perguntar se não é possível modelá-lo como um gráfico (reduzir as várias entidades em um nó). Talvez exista um caso entre o qual você faz uma referência circular, mas modelá-lo como gráfico não é apropriado, mas duvido muito disso.

Existe o perigo de as pessoas acharem que precisam de uma referência circular, mas na verdade não precisam. O caso mais comum é "O caso 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 isso não está correto. A razão é que sendo o endereço primário não é um relacionamento separado entre usuários e endereços, mas sim um atributo do relacionamento tem endereço . Por que é que? Porque o seu domínio é limitado aos endereços do usuário e não a todos os endereços que existem. Você escolhe um dos links e o marca como o mais strong (primário).

(Indo falar sobre bancos de dados agora) Muitas pessoas optam pela solução de dois relacionamentos porque entendem como "primária" como sendo um ponteiro único e uma chave estrangeira é uma espécie de ponteiro. Então chave estrangeira deve ser a coisa a usar, certo? Errado. Chaves estrangeiras representam relacionamentos, mas "primário" não é um relacionamento. É um caso degenerado de uma ordenação onde um elemento é acima de tudo e o resto não é ordenado. Se você precisasse modelar um pedido total, você o consideraria como um atributo de relacionamento, porque basicamente não há outra escolha. Mas no momento em que você degenera, há uma escolha e uma 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. O requisito de exclusividade deve ser imposto de outra maneira, por exemplo, por índices parciais únicos.

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

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

    
por 29.11.2013 / 04:02
fonte
2

Eu responderia a essa pergunta com outra pergunta:

Qual situação você pode me dar, onde manter um modelo de referência circular é o modelo melhor para o que você está tentando criar?

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

    
por 14.10.2010 / 02:39
fonte
1

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

    
por 29.11.2013 / 11:34
fonte
1

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

Considere a possibilidade de uma falha no código. Você não colocou erro de captura adequado em qualquer classe, ou porque você não desenvolveu seus métodos tão longe ainda, ou você é preguiçoso. De qualquer forma, você não tem uma mensagem de erro para informar o que aconteceu e precisa depurá-lo. Como um bom designer de programa, você sabe quais métodos estão relacionados a quais processos, portanto, você pode restringi-lo aos métodos relevantes para o processo que causou o erro.

Com referências circulares, seus problemas agora duplicaram. Como seus processos estão intimamente ligados, 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ê tem que gastar tempo testando as duas classes em conjunto para descobrir qual delas é realmente responsável pelo erro.

É claro que a captura adequada de erros resolve isso, mas somente se você souber quando um erro provavelmente ocorrerá. E se você estiver usando mensagens de erro genéricas, ainda não estará muito melhor.

    
por 29.05.2015 / 16:23
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 ingênua extremamente > de um coletor de lixo, não um que você jamais encontraria na prática.

    
por 14.10.2010 / 02:36
fonte
-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 que o programador procure por uma estrutura que não tenha a circularidade, embora possa não se encaixar perfeitamente no problema.

O problema real está na circularidade nas estruturas de dados de tempo de execução, em que alguns problemas não podem ser definidos de maneira a eliminar a circularidade. No final, porém - é o problema que deve ditar e exigir qualquer outra coisa que esteja 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.

    
por 11.05.2014 / 03:23
fonte