I'd like to create a model to represent math equations in XML. But I can't figure out how to without violating the open-closed principle.
For example:
<Math>
<Multiply>
<Constant Value="..."/>
<Constant Value="..."/>
<Constant Value="..."/>
</Multiply>
</Math>
That would be the XML representation of this math equation:
math = Constant * Constant * Constant
As you know, math equations are extensible. In other words if we introduce addition then this is also a valid math equation:
<Math>
<Multiply>
<Constant Value="..."/>
<Add>
<Constant Value="..."/>
<Constant Value="..."/>
</Add>
</Multiply>
</Math>
From the perspective of the C# model I know I want each Multiply
, Add
, Constant
, etc class to implement this common interface:
public interface IMath
{
double Solve();
}
So perhaps the models (sans serialization) will look like this:
public class Math : IMath
{
public IMath? Child { get; set; }
public double Solve() => Child?.Solve() ?? double.NaN;
}
public class Multiply : IMath
{
public List<IMath> Children { get; } = new();
public double Solve()
{
double? product = null;
foreach (var child in Children)
{
var childSolution = child.Solve();
if (product.HasValue)
{
product = product.Value * childSolution;
}
else
{
product = childSolution;
}
}
return product ?? double.NaN;
}
}
public class Constant : IMath
{
public double Value { get; }
public double Solve() => Value;
}
/* Use your imagination to see the `Add` class */
Now, I understand that I can do something like the following:
[Serializable]
[XmlRoot("Math")]
public class Math : IMath
{
[XmlElement("Multiply", typeof(Multiply))]
[XmlElement("Constant", typeof(Constant))]
public IMath? Child { get; set; }
public double Solve() => Child?.Solve() ?? double.NaN;
}
[Serializable]
[XmlRoot("Multiply")]
public class Multiply : IMath
{
[XmlElement("Multiply", typeof(Multiply))]
[XmlElement("Constant", typeof(Constant))]
public List<IMath> Children { get; } = new();
public double Solve()
{
double? product = null;
foreach (var child in Children)
{
var childSolution = child.Solve();
if (product.HasValue)
{
product = product.Value * childSolution;
}
else
{
product = childSolution;
}
}
return product ?? double.NaN;
}
}
...and so on.
But that doesn't seem very flexible. Because every single implementation
of IMath
has to know about every other implementation. Look what I have to
do when I throw the Add
class in the mix:
public class Math : IMath
{
[XmlElement("Multiply", typeof(Multiply))]
[XmlElement("Constant", typeof(Constant))]
[XmlElement("Add", typeof(Add))] // Now Math has to know about Add
public IMath? Child { get; set; }
...
}
public class Multiply : IMath
{
[XmlElement("Multiply", typeof(Multiply))]
[XmlElement("Constant", typeof(Constant))]
[XmlElement("Add", typeof(Add))] // Now Multiply has to know about Add
public List<IMath> Children { get; } = new();
...
}
public class Add : IMath
{
// Add has to know about everyone else :'(
[XmlElement("Multiply", typeof(Multiply))]
[XmlElement("Constant", typeof(Constant))]
[XmlElement("Add", typeof(Add))]
public List<IMath> Children { get; } = new();
...
}
Is there an alternative that doesn't violate the open-closed principle? I'm fine with using reflection.
If the above code doesn't compile and work perfectly then please wave your hands wildly at it until it does