C# – How to add properties to subclasses and access them without casting from a superclass

cdesigninheritanceobject-orientedobject-oriented-design

I'm trying to model a multi-dimensional point class in C#. I have about eight different types of points, and there may be more in the future. Right now, I have a superclass (PointBase) that holds all the common behaviors and data. This data includes only simple properties like doubles, strings, enums, and DateTimes (these are the dimensions). The subclasses (let's say 1Point, 2Point, etc.) all override at least things: the methods that have behavior for calculating two of these dimensions. If this behavior were the only changing thing, I could just use the Strategy + Factory pattern and be done with it (and honestly, I probably will refactor those two simple common behaviors out later).

Unfortunately, I'm stuck because some points add a property or two based on their type, or based on values of other properties in the superclass. For example, for point types 1-7, I don't need an additional Breakeven property, but for type 8, I do (this is problem #1).

In addition (this is problem #2), depending on the Product property (which exists and is set in the superclass, and is an enum), I may need to add a Term enum property that would apply to all subclass points. However, only one of my Product enum values needs a term. The others don't.

This situation is complicated by needing to store lists of these points in a List<PointBase> data structure that hides additional public properties. I'd have to cast each PointBase to a 1Point or 2Point or whatever when I took it out, and this (of course) breaks some design principles (separation, at the very least, I think).

Unfortunately, I'm also trying to display these different points on the same WPF DataGrid (in a scheme where the user can select 1Point or 2Point or whatever, and get the points' public properties displayed in the DataGrid). Although all of this code so far is contained within my model (in a separate DLL), I'm also unsure of how to represent this polymorphism in my view in a DataGrid. I'm totally fine with allowing the DataGrid to just autogenerate columns based on the points' public properties, but again, passing these points around in a List<PointBase> means that there's no way to get at those additional properties I need to add. I thought of instead adding a virtual Dictionary to the superclass called CustomAttributes to which I would add a string key and string value every time I needed to include some additional properties. But this is very annoying, because flattening a Dictionary for a DataGrid is really hard. I'm worried that this is the only way though…

From what I know (which is limited w.r.t. software engineering), I need something like an abstract factory to capture all this polymorpism. But I'm not exactly sure. To recap, I need:

  1. The ability to add public properties to certain subclasses based on their type. For example, for the 8Point subclass, I need to add a Breakeven property, but this property doesn't make sense for other subclasses.
  2. The ability to add public properties to all subclasses based on values of properties in the superclass. For example, if the Product (set in the superclass) is A, I need an additional property added to all subclasses called Term. This property does not apply or make sense for any other products.

I'm not married to inheritance here (in fact, I'd like to do something more elegant/simpler if possible, such as delegation + interfaces), so I'm open to any solutions. Thank you in advance!

EDIT: To give some more context, here is what my setup looks like right now:

public abstract class StatisticPointBase 
{
    public DateTime EventTime;
    public Product Product; // an enum
    protected double _delta;
    public string Name;
    public double FooSensitivity;
    public double BarSensitivity;
    public double BazSensitivity;
    public double FlopRatio;
    public double FlapCoefficient;
    ...
    public double Value;     // set by overridden CalculateValue
    public double Velocity;  // set by overridden CalculateVelocity        

    public PointBase(IDataRepository d, string name, double delta, Product p) 
    { 
        Name = name;   // just the name, like "Alpha" or "Beta"
        _delta = delta; // follows name, e.g. for Alpha it's always 0.63
        Product = p;
        FooSensitivity = d.GetFooSensitivity(p);
        ... // other setup
    }

    public abstract double CalculateValue();
    public abstract double CalculateVelocity();
}

public class AlphaStatisticPoint 
{
    public SimpleStatisticPoint()
    {
        Value = CalculateValue();
        Velocity = CalculateVelocity();
    }

    override double CalculateValue()
    {
        return (fooSensitivity + barSensitivity + bazSensitivity) * Velocity / 2;
    }

    override double CalculateVelocity()
    {
        return FlopRatio * (_delta / FlapCoefficient);
    }
}

public class BetaStatisticPoint 
{
    public SimpleStatisticPoint()
    {
        Value = CalculateValue();
        Velocity = CalculateVelocity();
    }

    override double CalculateVelocity()
    {
        return (FlopRatio * 100) * (_delta);
    }

    override double CalculateValue()
    {
        return fooSensitivity * Velocity;
    }
}

// other points...

public class ZetaStatisticPoint 
{
    public double Breakeven;

    public SimpleStatisticPoint()
    {
        Value = CalculateValue();
        Velocity = CalculateVelocity();
        Breakeven = CalculateBreakeven();
    }

    override double CalculateVelocity()
    {
        return (FlopRatio * 100) * (_delta);
    }

    override double CalculateValue()
    {
        return fooSensitivity * Velocity;
    }

    override double CalculateBreakeven()
    {
        return 2 * Math.sqrt(fooSensitivity / barSensitivity);
        // This is not defined for other types of statistics, and is only relevant for Zeta-type statistics. 
        // For other statistics, we can't even calculate the breakeven; there's no formula for it.
    }
}

public enum Product 
{ 
    Widget, 
    Watchet, 
    /* Woffet will be added in future, and needs a Term */
}

Stuff like Name and Delta should probably be set in the subclasses, since each subclass has one, and each value is unique to each subclass. For example, Alpha's delta value is always 0.63, Zeta's is always 0.21, etc. I'm trying to get away from inheritance though, and the real problem is that Breakeven exists only for Zeta (this is problem #1).

Furthermore (and this is problem #2), I may need to create and set a Term property, but only for Points that have a Product value of Woffet. It doesn't make sense for other products. This property, Term, would be added to all points, so if we're going with inheritance, I'd put it in the superclass. Although I'm starting to think I should just expand Product into a class, and then give it a Term property. But we run into the same problem for the new Product class then: Term only applies to the Woffet product, just like how Breakeven only applies to the Zeta statistic.

I realize that when one comes upon this behavior, it's usually the case the inheritance is not the answer (or so I've gathered?). So, I'm looking for a better way to model this. I was thinking of getting rid of all the subclasses but Zeta, and just giving the superclass a ValueCalculationStrategy and VelocityCalculationStrategy in the constructor (these would calculate Value and Velocity). But in this case, I'm still referring to Zeta as a StatisticPointBase, and the client doesn't know it's a ZetaStatisticPoint. So I'm still stuck. Maybe the problem is that I'm conceptually mixing statistics and points? I could make a separate ZetaStatistic class, and a ZetaBreakeven class, and then use these to compose a ZetaPoint class, but I don't know if this is the right thing to do.

Hopefully I've explained this adequately. If you have any questions, please ask in the comments and I'll do my best to expound on things.

Best Answer

Using strategy objects in the described way is a good start to simplify your PointBase class, I would not hesitate to introduce them even if they do not solve your problem with the BreakEven property.

For allowing custom properties, you could provide some kind "extension mechanism" in your PointBase class (which I would rename to Point after the redesign). In the most simple form, this could be just a list of custom properties for each Point object, together with a list of their values (and the list is empty by default). This might boil down to some kind of EAV model. EAV is sometimes seen as an anti-pattern, but for this specific case its usage is fine as long as you do not use it for the majority of properties, only for your "special custom properties".

See also this former question on "Programmers" about custom fields. In my answer there, I mentioned Martin Fowler's book "Analysis Patterns" - I am pretty sure it will help you for your problem, since it contains a very extensive discussion how to model measurements and observations, including (custom) properties and similar things.

Related Topic