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!
fonte
Respostas:
Há 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.
fonte
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.
fonte
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).
fonte
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.
fonte
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.
fonte
Node
é uma classe, que tem outras referênciasNode
para 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.É 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 ...
fonte
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.
fonte
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.
fonte
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)
fonte
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.
fonte
À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.
fonte
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.
fonte
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.
fonte
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.
fonte