I don't often come across a data modeling problem where circular referencing appears to be a good solution. In general I find that arguments that attempt to justify circular references appear contrived (Apartment <-> Person), and because of that lacking any true real world applicability. That said, circular referencing isn't always an attempt at a data modeling solution, more often than not I find it's an attempt at an encapsulation solution.
Often when defining complex models code files can become large and in an effort to make the repository more digestible functionality and properties are encapsulated into smaller bite-sized classes. The problem arises when some of those bite-sized classes require access to data within the original scope. So you attempt to provide access to it:
class Human {
private var brain: Brain! // Very complex component. So naturally it's moved into its own file.
private let heart: Heart
private let lungs: Lungs
// ... tons of code ...
init() {
self.heart=Heart()
self.lungs=Lungs()
self.brain=Brain(human: self)
}
New file:
class Brain {
private let human: Human
// ... tons of code ...
init(human: Human) { self.human=human }
}
Obviously this is a problem because it causes a retain cycle. You can try to make Brain.human
unowned
, but that code becomes fragile. You can make it weak
, but now any members that return something in Brain
that use human
will have to return Optionals
instead, polluting the API unnecessarily with uncertainty.
What I'm about to pitch here is a rough sketch of a mechanism to a solution to this problem, not necessarily the syntax that that solution will take the form of.
So, instead of making just Brain.human
unowned
we can also make Human.brain
unowned
and also add something else to the procedure during Human.brain's
initialization:
class Human {
private unowned var: Brain!
// ... tons of code ...
init() {
// ... init code ...
self.brain={
let brain=Brain(human: self)
pool_reference_counts(self, brain) // Secret sauce.
return brain
}()
}
}
What pool_reference_counts(_:)
would do is sum the reference counts of the given parameters into a single count and make the given objects share that count. Every time a new brain or human strong reference would arise, that count would increment by 1, effectively incrementing both their reference counts. By the same token when a strong reference to either of them is destroyed that count would decrement by 1. And the only time Human.brain
or the owning Human
would be deallocated is when there are no more strong references to either of them.
This way we can guarantee that Brain.human
will always exist, and rest assured that we won't run into any issues when accessing it in the body of Brain
— the same going for Human.brain
in the body of Human
as well.
Disregarding the specific syntax I used to express this shared reference counting concept, I think this mechanism is a means of eliminating the headaches induced by trying to shoehorn circular referencing in a language that is not memory managed, and will make us less likely to choose between encapsulation, safety, or model integrity.