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:
- The ability to add public properties to certain subclasses based on their type. For example, for the
8Point
subclass, I need to add aBreakeven
property, but this property doesn't make sense for other subclasses. - 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) isA
, I need an additional property added to all subclasses calledTerm
. 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.
8Point
has aBreakeven
property, but the others don't. I often need to switch between which Point subclass I'm looking at (they're being displayed in a graph and DataGrid, switched around by user choice), so I have aList<PointBase>
.List<PointBase>
hides theBreakeven
property of8Point
though. So, I can't showBreakeven
in the DataGrid (or do other things withBreakeven
, because I don't know that I'm dealing with an8Point
), but the user must see it. Furthermore, in the future, I expect there to be other custom properties for some points, but not others, so this problem will persist. This is where I'm stuck. I either need different lists for each subclass (bad), or to addBreakeven
toPointBase
(bad, b/c pts 1-7 don't have it). So I'm looking for a better way to model the points