ckemper ckemper - 3 months ago 28
C# Question

C# properly coupling Parent/Children objects without sacrificing scalability

Consider two classes:

public class Parent { }
public class Child { }


Let's say that Parent owns a collection of Child elements. Parent has its own properties, methods, etc. Child cannot exist without a Parent instance, as some of its properties depend on a Parent instance...

public class Parent
{
// ImmutableList is used to simplify INotifyPropertyChanged which is used in actual code
public ImmutableList<Child> Children { get; set; }

// more ...
}

public class Child
{
private Parent Parent { get; }

// Other properties using Parent...

public Child(Parent parent)
{
if (parent == null)
throw new ArgumentNullException(nameof(parent));

Parent = parent;
}

// more...
}


Now this is all fine and dandy until we decide we need Parent and Child to be extensible for future implementation changes, etc. Let's make them abstract so we can do just that. Now there is another problem, though. Our
Children
and
Parent
properties use the abstract types. This isn't going to cut it when we derive later. If we add properties to a derived Parent and derived Child, we want to somehow associate these derivations with each other so they can see each others properties. Alright, no problem, let's make them generic!

public abstract class Parent<C> where C : Child
{
// ImmutableList is used to simplify INotifyPropertyChanged which is used in actual code
public ImmutableList<C> Children { get; set; }

// more ...
}

public abstract class Child<P> where P : Parent
{
private P Parent { get; }

// Other properties using Parent...

public Child(P parent)
{
if (parent == null)
throw new ArgumentNullException(nameof(parent));

Parent = parent;
}

// more...
}


Ooh well that doesn't compile. Our constraints are referencing Parent and Child without any generics. Try to fix this for yourself. You'll land yourself right about here...

public abstract class Parent<P, C> where P : Parent<P, C> where C : Child<P, C>
{
// ImmutableList is used to simplify INotifyPropertyChanged which is used in actual code
public ImmutableList<C> Children { get; set; }

// more ...
}

public abstract class Child<P, C> where P : Parent<P, C> where C : Child<P, C>
{
private P Parent { get; }

// Other properties using Parent...

public Child(P parent)
{
if (parent == null)
throw new ArgumentNullException(nameof(parent));

Parent = parent;
}

// more...
}


Here we have recurring generic types (aka CRTP - Curiously Recurring Template Pattern; read more about it here https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern and here Is letting a class pass itself as a parameter to a generic base class evil?). We have this in two degrees however, since we are coupling two classes.

This fully solves the problem by allowing complete control over shared code through the inheritance hierarchy, while keeping a generic interface to it all. I'm not sold on it though, because it is outrageously complex and, without documentation, would be a big pain for other developers to decipher. Not sold on how complex this is? Watch...

Simple scenario. Let's derive a custom Parent and Child, not caring about their reuse (they'll be forever coupled to each other, and no other Parent derivation or Child derivation can step in share any code.) Here it is:

public class ManParent : Parent<ManParent, ManChild> { }

public class ManChild : Child<ManParent, ManChild>
{
public ManChild(ManParent parent) : base(parent)
{
}
}


This is great. It allows us to derive Parent and Child, benefiting from shared code, and still have compile-time access to elements from ManChild in ManParent and vice-versa. What if we want to derive another Child that uses this same Parent though? Can't do it since both classes have specified the other; they are forever-and-ever, permanently coupled.

In order to derive a Parent that can be used by multiple derived Child classes (or Child that can be used by multiple derived Parent classes), we can derive one generic variable at a time...

public class CoolParent<C> : Parent<CoolParent<C>, C> where C : CoolKid<C> { }

public abstract class CoolKid<C> : Child<CoolParent<C>, C> where C : CoolKid<C>
{
public CoolKid(CoolParent<C> parent) : base(parent)
{
}
}

public class CoolKidOne : CoolKid<CoolKidOne>
{
public CoolKidOne(CoolParent<CoolKidOne> parent) : base(parent)
{
}
}

public class CoolKidTwo : CoolKid<CoolKidTwo>
{
public CoolKidTwo(CoolParent<CoolKidTwo> parent) : base(parent)
{
}
}

public class CoolKidThree : CoolKid<CoolKidThree>
{
public CoolKidThree(CoolParent<CoolKidThree> parent) : base(parent)
{
}
}
// You could even go nuts and derive a CoolParent for each CoolKid, but I digress


This allows the CoolKid to consolidate code using CoolParent that is generic across all CoolKids, and allows each CoolKid to extend their use of CoolParent, all while keeping the CoolParent's use of CoolKid in one place.

I don't know about you, but I'm about ready to jump off a bridge :)

Anyway, so here's the question. Is this my only good design option for something like this? This seems overly complex, but I really tend to see duplicating large amounts of code as a no-go and turn to abstract classes and generic types to solve that problem.

The only other option I can see would be to make Parent generic and abstract and wrap all access to Child. Then I could keep all Parent-Child-dependent properties in Parent instead of Child, so Child could be generic with no knowledge of Parent, so no CRTP. However this means that, as Child grows more and more complex, Parent will grow more and more complex. Not to mention all wrappers would have to be able to route to the correct Child via indexing of some sort since Child itself will be hidden. This seems like it would introduce too much room for run-time errors IMO and reminds me of passing around 'handles' in a non-object-oriented environment, ick.

Edit, more information about the actual design:

The actual application that I am building is an MVVM(C) application with views, view models, and then a few controllers (to wire up the view models to the larger model). The larger model has a few important aspects:


  • It must have a generic interface. At the end of the day, real implementations of this model talk to any number of devices over a serial port. This communication involves on-demand commands/queries as well as live 'streaming' of data at rates of about 10 times per second.


    • It must be abstract enough to be able to represent features that are 100% software (computed in memory) OR simply queried from the devices themselves (which often compute many things). These things can change a lot during the lifetime of the software.

    • It might need to be thread safe using locks. Currently, I am planning on requiring a synchronization context in the constructor and subsequently only mutating the model from the context in order to avoid thread safety issues. However, it is not unlikely that I may need to put in locking code around all the properties (about 20ish, but could grow in the future).

    • It must be able to discern between internal changes (changes that come from the device(s) and mutate the model over time) VS external changes (changes that originate from the user requests like the UI, controllers, command line, etc.)




The above requirements cause a lot of problems with "keep it simple". Normally, with something like this, I would try to avoid generics altogether and just duplicate code. However, in this case, even without locks, I'm looking a good many hundred lines of boilerplate for INotifyPropertyChanged and additional mutating methods (for the external mutation requests), and asynchronous code that handles the mutation over time. Repeating all of this code every time I need a slightly different or drastically different implementation of the interface would be more than a nuisance, it would be a maintenance/development nightmare. This software requires too much reliability and stability to risk that kind of design. This is why I'm even considering something like second degree CRTP.

With that in mind, hopefully it's a little easier for people to give their two cents on this issue.

Edit 2:

I have been further pondering alternatives to this, and haven't come across anything that provides the single most important benefit that this design does: compile-time restricted feature sets for a given Parent-Child combination. I am starting to think that this, albeit very complex, is the best design route. So I pose a more narrowed question: Complexity aside, what issues would a design like this introduce? So far, because of the strong typing, it seems that the only bad thing that could come out of this would be confusion to a future developer, resulting in compile-time errors if they fail to derive Parent/Child correctly. This is where documentation would be more than enough to fill the gap.

Answer

After considering what Aron said about the Parent/Child relationship not being true OOP, I have veered away from a strongly-typed, two-way coupling. Instead, I have isolated the Child so that only the Parent knows of the Child. Any instances where Child needs access to Parent have been moved to the Parent class (as methods which require a Child instance). This makes Parent generic only in one variable (C : Child) and Child not generic at all, so no CRTP. While this will make the Parent class much more complex, we retain the advantages of a compile-time relationship between a derived Parent and derived Child.

Something like this:

public abstract class Parent<C> where C : Child
{
    public ImmutableList<C> Children { get; set; }
}

public abstract class Child
{
}
Comments