3

I've heard circular references are generally an issue, however I was wondering if this was true for interfaces that reference other interfaces, for example:

IQuestion{
    IAnswer getCorrectAnswer();
    IList<IAnswer> getAllAnswers();
}

IAnswer{
    IQuestion getQuestionInResponseTo();
}

Is there any case where this would be an issue? It doesn't seem to me to be resolveable if it were. My best guess is that this wouldn't be an issue as interfaces don't demand much in terms of referencing. Thanks in advance.

4 Answers4

5

It's an issue as long as you have to check the integrity/validity of both references as soon as one of them change (and might want to look out for possible endless recursion doing so). As they seem to be pretty static, it's not (much of) an issue; you'd probably need to double-check that during the construction of the model objects. But don't forget to do it. You don't want Question A referencing the Answer A, and Answer A referencing Question B as its "parent".

A bit off-topic: why keeping both references? Is it just to save some time to get the question that an answer belongs to? I think in the contexts you'd probably be using the question and its answers in, the context already has the references to both the question and the answers, so that relationship shouldn't be hard to extract from them, and would make your model design much less error-prone and easier to maintain.

2

I don't think Its technically a circular reference. It will compile.

But there are some issues you should be aware of with this kind of structure. For example serialisation.

A naive approach could lead to an infinite loop if you refer back to the parent object.

Additionally you maybe have to account for a null value. If you have a tree of these objects the final leaf will have a null value.

I would avoid such structures outside of those specifically designed for a tree, linked list or recursion.

Where you just want an easy reference to the parent object I would rather loop through the parents to find it.

Ewan
  • 83,178
1

It is clearly a circular reference at the level of the interfaces. Similar to recursive data types — which have circular references at the type level — that does not necessarily mean that the instance objects are themselves are engaged in circular references (though they certainly could be).

In many circumstances (but certainly not all), there is nothing "wrong" with circular references, even though they will generally exhibit some construction & destruction ordering issues (i.e. precluding some of the instance objects involved from being immutable objects).

It can help to have a notion of parent and child.  While the parent references the children, and the child reference the parent, we usually agree that deleting a child deletes only (the child itself and) the one parent-child reference (and not the parent itself), whereas deleting the parent can mean deleting not just the parent-child references, but also all its current children as well.

This means that the parent & child references are directional and asymmetrical, and in some sense, one is effectively "stronger" than the other despite the circularity, as one includes a notion of ownership that other does not.

While some databases allow us to specify this "strength" as an expression of constraint; our programming languages, by contrast, mostly do not allow us to differentiate between parent-to-child and child-to-parent references (they are both merely references).  If they did allow such differentiation, we could consider parent/child and child/parent references as non-circular in the light of their asymmetrical nature.


Some reference counting algorithms break cycles via implicitly tagged or explicitly declared strong references.


In the small, cycles are not too bad when using a garbage collected language.  For manual memory management, however, especially reference counting, cycles are an issue.

In the large, such as when we get to cycles in libraries (one library depends on another which depends on it...), these can be problematic in particular in regards to initialization ordering.  Such problems tend to be exacerbated for circularities among operating systems components like drivers and/or subsystems: not that it cannot be done but it increases the complexity, while also hampering the ability for subsetting: creating a smaller version of the operating system for a smaller environment.

Erik Eidt
  • 34,819
0

It's only an issue if you're using a language where circular references can cause memory-leaks (like C++) or if you're doing something such as loading these objects from a database and end up in an infinite loop if your ORM is not aware of how to handle this properly. All the issues I'm aware of can be worked around as long as you are aware of them, using things like weak/non-owning pointers or lazy initialisation.