Search code examples
c#hierarchyencapsulationfriendmember-access

Easier way to write encapsulated parent/child data structure?


From time to time I find myself often writing a data structure of "parents" and "children", where:

  • A parent has references to 0 to N distinct children.
  • A child has a reference to 0 parents or 1 parent.
  • The reference must be mutual. For any given parent, any child that it references must also reference the given parent back. For any given child, the parent that it references must reference the given child back.
  • It's impossible to violate the above rules through use of members accessible from outside the two class declarations (non-private), aside from use of Reflection.

The mental steps one might take before implementing something like this might start with something like this:

public class Parent
{
    private readonly List<Child> _children = new List<Child>();

    public readonly ReadOnlyCollection<Child> Children = _children.AsReadOnly();
}

public class Child
{
    private Parent _parent;

    public Parent Parent
    {
        get
        {
            return _parent;
        }
        set
        {
            if(value == _parent)
                return;

            if(_parent != null)
            {
                _parent._children.Remove(this);
                _parent = null;
            }

            if(value != null)
            {
                value._children.Add(this);
                _parent = value;
            }
        }
    }
}

Of course, this will not compile, since Parent._children is private. But, you wouldn't want to make it anything but private, since allowing access outside of Child or Parent would make it possible to violate the rules in an implementation or elsewhere.

So, a solution I came up with is to nest Child in Parent. Nested classes can access private members of the class its nested within:

public class Parent
{
    private readonly List<Child> _children = new List<Child>();

    public readonly ReadOnlyCollection<Child> Children = _children.AsReadOnly();

    public class Child
    {
        private Parent _parent;

        public Parent Parent
        {
            get
            {
                return _parent;
            }
            set
            {
                if(value == _parent)
                    return;

                if(_parent != null)
                {
                    _parent._children.Remove(this);
                    _parent = null;
                }

                if(value != null)
                {
                    value._children.Add(this);
                    _parent = value;
                }
            }
        }
    }
}

The question I'm asking is, are there any alternative ways to write this that accomplish the same goals which have fewer or less significant drawbacks than this approach? The main drawbacks I'm finding to this approach are:

  • This can lead to large scripts, though use of partial can help.
  • This can lead to deeper nesting than desired.
  • This can lead to verbose class names. To access Child outside of Parent, you have to use Parent.Child. In cases where the class names are long, and especially when generics are used, this can lead to very ugly code.
  • Use of generics (for example, to achieve compile-time type safety) can get messy. This appears to at least partially stem from the fact that Child is nested and Parent<T1>.Child is a distinct type from Parent<T2>.Child, and when you want type safety to be mutual this can lead to really ugly use of generics, or needing to fall back to using runtime-enforced type safety (though it can be encapsulated away, usually, e.g., using a non-generic abstract base where public accessors are instead protected).

On a side note, this may be a good example of where the use of friend to extend access rights would simplify these problems!


Solution

  • I like to use a common, private interface to expose properties like this:

    public class SomeBigOuterClass {
        private interface IChildFriend {
            void SetParent(Parent parent);
        }
    
        public class Parent {
        }
    
        public class Child : IChildFriend {
            void IChildFriend.SetParent(Parent parent) {
                this.parent = parent;
            }
            private Parent parent;
        }
    }