5

I have 2 sets of 2 classes where each pair has a super/sub-class relationship, and the orthogonal pair has a dependency relationship. What I am trying to determine is what to do with the constructors and/or bodies of the properties to keep the model as simple as possible with minimal data duplication.

Here's the structure in code:

public class Base1 {
    public List<Base2> MyBase2Things { get; set; }
    // Do things with Base2 objects
}

public class Sub1 : Base1 {
    public List<Sub2> MySub2Things { get; set; }
    // Do things with Sub2 objects and also with Base2 objects
}

public class Base2 {
    public Base1 MyBase1 { get; set; }
    // Do things with the Base1 object
}

public class Sub2 : Base2 {
    public Sub1 MySub1 { get; set; }
    // Do things with the Sub1 object
}

I have considered overriding the base properties in the sub-classes, but that doesn't fit very cleanly because the properties in the sub-classes don't have the same signature and so I would have to add properties.

I have also considered setting the base property in the sub-class constructor and set methods, but there is no way for the sub-class property to be updated if the base-class's property is updated.

What other options are there, and which is the cleanest (and why)?

Note: The above code is greatly simplified to illustrate the problem. There are additional properties and methods on the real classes, but this subset is the essence of the trouble I'm having.

cdeszaq
  • 30,869
  • 25
  • 117
  • 173
  • I think the simple answer is you can't. In .Net 4.0 covariance helps you out somewhat but ... here's hoping I'm wrong. http://stackoverflow.com/questions/245607/how-is-generic-covariance-contra-variance-implemented-in-c-4-0 – Jodrell May 18 '11 at 13:58
  • @Jodrell - Ok, so if I can't do it as cleanly as I would like due constraints imposed by the CLR and security concerns, what is the next best that I can do that minimizes data duplication and ensures data integrity while still following good design best practices? – cdeszaq May 18 '11 at 14:01
  • 1
    It would help to know what you are trying to accomplish. Liskov substitution principle says any method that works on Base1 should work on Sub1, and since you have different lists, that isn't true. I would avoid exposing your list publicly anyway. Can you work around it by having explicit Add/Remove/Get methods? – NerdFury May 18 '11 at 14:07
  • @NerdFury - The Sub1 needs to deal with things specific to Sub2, but Base1 only needs to deal with Base2 things. Since Sub1 and Sub2 are sub-classes, they inherit the lists from their parents, so Liskov substitution isn't violated. I agree that exposing the lists is a bit smelly, but that isn't the main issue. However, I think Add/Remove/Get methods may be the right track. – cdeszaq May 18 '11 at 14:23
  • 3
    "parallel inheritance structure" always seem good at the time, but they tend to bite me back sooner or later! – Ian Ringrose May 18 '11 at 14:28
  • you could go against guildlines and declare implicit conversion operators, very messy and plenty of work, not a real answer. – Jodrell May 18 '11 at 14:54
  • @Ian Ringrose - How have you gotten around it in the past? What have you done instead? – cdeszaq May 18 '11 at 15:10

2 Answers2

2

I agree with Yaur that generics may help. As far as your options and keeping the model simple as possible - this probably depends on the specifics like the responsibilities of your 4 classes.

Let's say you're dealing with parent/child relationships of various vehicles & vehicle parts.

Scenario 1: The inherited relationship brings in orthogonal capability.

public class ItemParent {  // formerly Base1
    public List<ItemChild> MyChildren {get; set;}
}

public class ItemChild {  // formerly Base2
    public ItemParent MyParent {get; set;}
}

public class Car : ItemParent {  // formerly Sub1
    public List<CarPart> MyParts {get; set;}
}

public class CarPart : ItemChild {  // formerly Sub2
    public Car ParentCar {get; set;}
}

Of course, Cars should specifically know about CarPart, not ItemChild. So you fall back on generics here.

public class ItemParent<T> where T : ItemChild {
    public List<T> MyChildren {get; set;}
}

public class ItemChild<T> where T : ItemParent {
    public T MyParent {get; set;}
}

public class Car : ItemParent<CarPart> {}
public class CarPart : ItemChild<Car> {}

public class Truck : ItemParent<TruckPart> {}
public class TruckPart : ItemChild<Truck> {}

You can call subclass.MyChildren[] just fine, or make a MyParts property which delegates to MyChildren.

In this example, I think the model is pretty simple due to the fact that the parent/child metaphor is pretty easy to grok. Plus, if you add Truck-TruckParts (or Household-Resident, Shape-Line, etc.) you're not really increasing the complexity.

An alternative here would be to move the parent/child "responsibility" to a collection object (possibly custom), like so:

public class ParentChildCollection<TParent, TChild> {}

public class Car {
    private ParentChildCollection<Car, CarPart> PartHierarchy;
    public List<CarPart> MyParts {get { return PartHierarchy.GetMyChildren(this); } }
}

public class CarPart {
    private ParentChildCollection<Car, CarPart> PartHierarcy;
    public Car ParentCar {get { return PartHierarchy.GetMyParent(this); }}
}

The downside here is that, while clean, Truck and Car might not share a lot of code (if that's what you were wanting).

Scenario 2: The inherited relationship is about specializing to a parallel item.

public class Car {  // formerly Base1
    public List<CarPart> MyParts {get; set;}
}

public class CarPart {  // formerly Base2
    public Car MyParent {get; set;}
}

public class Truck : Car {  // formerly Sub1
    public List<TruckPart> MyParts {get; set;}
}

public class TruckPart : CarPart {  // formerly Sub2
    public Truck MyParent {get; set;}
}

In this case, Truck and Car do share more code. But this starts running into signature problems that aren't easily solved even with generics. Here, I'd consider making the base class more generic (Vehicle-VehiclePart). Or consider refactoring this second scenario into the first scenario. Or use the collection for parent/child management and the inheritance stictly for Car-Truck code consolidation.

At any rate, I'm not really sure that either scenario matches your case. At least some factor are based on how you have (and how you can) arrange your relationships.

Matt
  • 4,388
  • 1
  • 15
  • 8
0

Generics may be able to help you with at least part of this... something like:

public class Base1<T>
    where T: Base2
{
    public List<T> MyThings { get; set; }

    protected Base1(List<T> listOfThings)
    {
        this.MyThings = listOfThings;
    }
}

public class Sub1 : Base1<Sub2>
{
    public Sub1(List<Sub2> listofThings):
        base(listofThings)
    {

    }
}

making it work where you need to subclass in both directions can get tricky (and messy) quickly, but will look something like:

// Base 1 hierachy
abstract public class Base1
{
    protected abstract Base2 GetBase2(int index); //we can't return the list directly
}

public class Base1<Base2Type> :Base1
    where Base2Type : Base2
{
    public List<Base2Type> MyBase2s { get; set; }

    protected Base1(List<Base2Type> listOfThings)
    {
        this.MyBase2s = listOfThings;
    }

    protected override Base2  GetBase2(int index)
    {
        return MyBase2s[index];
    }

}

public class Sub1<MySub1Type,MySub2Type> : Base1<MySub2Type>
    where MySub1Type : Sub1<MySub1Type,MySub2Type>
    where MySub2Type : Sub2<MySub1Type, MySub2Type>
{
    public Sub1(List<MySub2Type> listOfThings):
        base(listOfThings)
    {
        this.MyBase2s = listOfThings;
    }
}

public class Sub1 : Sub1<Sub1,Sub2>
{
    public Sub1(List<Sub2> listofThings):
        base(listofThings)
    {

    }
}


// base 2 hirachy
abstract public class Base2
{
    protected abstract Base1 MyBase1 { get; }
}

public class Base2<Base1Type,Base2Type> : Base2
    where Base1Type: Base1<Base2Type>
    where Base2Type : Base2
{
    public Base1Type myBase1;

    protected override Base1 MyBase1{ get {return myBase1;} }
}

public class Sub2<Sub1Type, Sub2Type> : Base2<Sub1Type,Sub2Type>
    where Sub1Type : Sub1<Sub1Type,Sub2Type>
    where Sub2Type : Sub2<Sub1Type,Sub2Type>
{

}

public class Sub2 : Sub2<Sub1,Sub2>
{

}
Yaur
  • 7,333
  • 1
  • 25
  • 36