0

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!

Hatchling
  • 191
  • 1
  • 7
  • 1
    How about adding a public method in the parent Remove(Child) that will remove the child from the list but inside the parent – Dan Hunex Feb 28 '17 at 22:11

3 Answers3

2

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;
    }
}
Tim
  • 5,940
  • 1
  • 12
  • 18
  • Interesting, I never considered using interfaces this way. Still uses nesting mind you, but it'd likely to be easier to manage as the class becomes more complex with generics and inheritance. – Hatchling Feb 28 '17 at 22:20
  • 1
    It is my way of bringing the "friend" idea back over to C#, as you can see from my naming conventions... – Tim Feb 28 '17 at 22:21
  • Dang, now I feel like I gotta refactor my code which used the old pattern :( – Hatchling Feb 28 '17 at 22:29
  • I have to say, this technique actually does a pretty good job of managing the type complexity when used with generics. Putting type constraints on the parent class Parent when Child is nested within has problems you need to work around, whereas putting them on SomeBigOuterClass doesn't have those problems. (Or at least, it doesn't appear to.) Very helpful, thanks! – Hatchling Mar 01 '17 at 02:37
1

As I stated in the comment, add a Remove method in the parent and that will at least won't expose the whole children list. Something like:

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

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

        public void Remove(Child child)
        {
            if(child !=null) 
            {
             _children.Remove(child);
            }
        }
    }
Dan Hunex
  • 5,172
  • 2
  • 27
  • 38
  • This would allow one to break the mutual reference requirement. One could remove a child, but the child would still reference the parent. – Hatchling Feb 28 '17 at 22:19
  • 1
    I suppose both the child and the parent could have Add/Remove methods that conditionally call one another based on which one was called first (which could be determined via examining the parent or child references) though I can see that getting hard to follow and prone to infinite recursion. – Hatchling Feb 28 '17 at 22:22
  • It won't be infinite recursion, you will remove if it is not null. So removing parent, means if it is not null, if it is null you won't do it. so no infinite recursion – Dan Hunex Feb 28 '17 at 22:24
  • I'm sure it's doable, but it gets a little dicey when you consider that a single transaction could have three entities involved (two parents and one child). – Hatchling Feb 28 '17 at 22:25
0

And of course, as stated in comments and in previous answer, you will also need to encapsulate adding childs to parent, which would require public Add(Child) method

Dmitry Pavlushin
  • 612
  • 8
  • 23