Sean Werkema
Sean Werkema

Reputation: 6125

Readonly cross-class references: A Catch-22

We have a set of classes that exclusively use readonly fields to help ensure that the classes are safe and stable across the scope of the software, especially across threads. Read-only data, as we all know, is a Good Thing™.

In particular, these classes use readonly to ensure that other developers working in the codebase cannot alter them — even by accident — including within methods in the same class: It's a safety check to keep coders of lesser skill or of lesser codebase knowledge out of trouble, which is why these classes prefer to use readonly rather than just to have a property with a private set method. There is no better safety check than the compiler itself telling you "NO".


But there's an interesting Catch-22 that shows up when everything is readonly, which is that parent/child relationships become impossible if the references are supposed to point both ways. Consider the code below:

public class Parent
{
    public readonly IList<Child> Children;

    public Parent(IEnumerable<Children> children)
    {
        Children = Array.AsReadOnly(children.ToArray());
    }
}

public class Child
{
    public readonly Parent Parent;

    public Child(Parent parent)
    {
        Parent = parent;
    }
}

This is legal C# code. It will compile. And it's the right way to model the relationship.

But it's impossible to instantiate either Parent or Child correctly: Parent cannot be instantiated until you have a full collection of children to attach to it, but each Child cannot be instantiated without a valid Parent to hang it off of.

(In our case, the data forms trees, but this problem shows up in many other scenarios: Variations on this same problem show up in readonly doubly-linked lists, readonly DAGs, readonly graphs, and so on.)


Typically, I've worked around this Catch-22 using some kind of hack. Options I've used in the past include:

In short, there's not an easy answer for this problem that I've been able to find. I've used each of the above solutions at various points in the past, but they're all sufficiently distasteful that I'm wondering if anyone has a better answer.


So, in summary: Is there a better solution that you've found to the Catch-22 of having exclusively readonly cross-class references?


(And yes, I know that in some ways, I'm trying to use technology to solve a people problem, but that's not really the point: The question is whether the cross-class readonly pattern can be made functional, not whether it should exist in the first place.)

Upvotes: 0

Views: 100

Answers (3)

Iridium
Iridium

Reputation: 23721

As mentioned in my comment, another possibility which does not require compromising the readonly-ness of your parent/children references is to construct your "Child" objects by way of a factory method passed into the "Parent" object:

public class Parent
{
    private readonly Child[] _children;

    public Parent(Func<Parent, IEnumerable<Child>> childFactory)
    {
        _children = childFactory(this).ToArray();
    }
}

public class Child
{
    private readonly Parent _parent;

    public Child(Parent parent)
    {
        _parent = parent;
    }
}

There are many ways to generate the factory passed to the Parent constructor, e.g. to just create a parent with 5 children you might use something like:

private IEnumerable<Child> CreateChildren(Parent parent)
{
    for (var i = 0; i < 5; i++)
    {
        yield return new Child(parent);
    }
}

...

var parent = new Parent(CreateChildren);

...

As was also mentioned in the comments, this mechanism is not without its potential downsides - you must ensure that your parent object is fully initialized before calling the child factory in the constructor, since it may perform actions (call methods, access properties, etc.) on the parent object, and so the parent object must be in a state where this will not result in unexpected behavior. Things become more complicated if someone derives from your parent object since the derived class will never be fully initialised before the child factory is called (since the constructor in the base class is called before the constructor in the derived class).

Your mileage may therefore vary, and it's up to you to determine whether the benefits of this approach outweigh the costs.

Upvotes: 1

Scott Chamberlain
Scott Chamberlain

Reputation: 127583

What you are really talking about are immutable classes, not just read-only fields. So to solve your problem take a look at what Microsoft's immutable collections do.

What they do is they have "builder classes" which are the only things allowed to break the rules of immutability, it is allowed to do it because the changes it makes are not visible external to the classes till you call ToImmutable() on it.

You could do similar with a Parent.Builder that could have a public void AddChild(Foo foo, Bar bar, Baz baz) which adds a child to the internal state of the parent.

Once everything is added you call public Parent ToImmutalbe() on the builder and it returns a Parent object that has all of it's immutable children built up.

Upvotes: 1

Fabio
Fabio

Reputation: 32455

I think this is more design problem than language.

Does Child class really need to know about Parent?
If so, then what kind of information or functionality it needs. Can it be moved to the Parent?
If you follow "Tell not ask" rule/principle, then in Parent class you can call Child methods and pass Parent's information as parameters.

If you still stay will current approach then as your Parent and Child classes have "circular" dependency which can be resolved by introducing new class which combine them both.

public class Family
{
    private readonly Parent _parent;
    private readonly ReadOnlyCollection<Child> _childs;

    public Family(Parent parent, IEnumerable<Child> childs)
    {
        _parent = parent;
        _childs = new ReadOnlyCollection<Child>(childs.ToList());
    }
}

Upvotes: 0

Related Questions