I'm writing a C# program that calculates total worth of financial assets. Each asset is represented by asset ID. There are calculator classes specialized for calculating worth of specific asset ID.
public interface IAssetId
{
string GetSymbol();
}
public class Currency(string currency) : IAssetId
{
public string GetSymbol() => currency;
}
public class EtfTicker(string ticker) : IAssetId
{
public string GetSymbol() => ticker;
}
public interface IAssetWorthCalculator<in TAssetId> where TAssetId : IAssetId
{
decimal CalculateWorth(TAssetId assetId);
}
public class MoneyWorthCalculator : IAssetWorthCalculator<Currency>
{
public decimal CalculateWorth(Currency assetId) ...
}
public class EtfWorthCalculator : IAssetWorthCalculator<EtfTicker>
{
public decimal CalculateWorth(EtfTicker assetId) ...
}
Now let's say I have a list of different asset IDs for which I want to calculate total worth.
List<IAssetId> assetIds = [
new Currency("USD"),
new EtfTicker("SPY")
];
decimal worth = TotalWorthCalculator.GetTotalWorth(assetIds);
The total worth calculations would be performed in a class that would look something like this:
public class TotalWorthCalculator
{
public decimal GetTotalWorth(IList<IAssetId> assetIds)
{
decimal sum = 0;
foreach (IAssetId id in assetIds)
{
IAssetWorthCalculator<IAssetId> worthCalculator = ???
decimal worth = worthCalculator.CalculateWorth(id);
sum += worth;
}
return sum;
}
}
I'm trying to figure out a way to implement a class, that returns a proper worth calculator for given asset ID e.g.
MoneyWorthCalculator
objectEtfWorthCalculator
Problem 1:
I can't get a common type for the worth calculators (to use it in a class that returns proper calculators for given asset ID). So trying to do the following raises a compiler error (because IAssetId
is a more base type than Currency in MoneyWorthCalculator
):
IAssetWorthCalculator<IAssetId> worthCalculator = new MoneyWorthCalculator();
Problem 2: Each calculator has a CalculateWorth method, that accepts only a specific parameter type, so MoneyWorthCalculator won't accept IAssetId.
I was thinking about using a non-generic versions of the worth calculators, but that would require method parameter type checking, which would break the Liskov Substitution Principle:
public class MoneyWorthCalculator : IAssetWorthCalculator
{
public decimal CalculateWorth(IAssetId assetId)
{
if (assetId is not Currency)
{
throw new ArgumentException();
}
return 10;
}
}
Without making changes to the defined abstractions, you can change the TotalWorthCalculator
to the following:
public class TotalWorthCalculator
{
private readonly IList<object> calculators;
public TotalWorthCalculator(IList<object> calculators)
{
this.calculators = calculators;
}
public decimal GetTotalWorth(IList<IAssetId> assetIds) =>
assetId.Select(this.Calculate).Sum();
private double Calculate(IAssetId id)
{
Type calculatorType =
typeof(IAssetWorthCalculator<>).MakeGenericType(id.GetType());
dynamic calculator = this.calculators
.Single(c => calculatorType.IsAssignableFrom(c.GetType());
return calculator.CalculateWorth((dynamic)id);
}
}
This solution uses Reflection and dynamic typing. You can instantiate the TotalWorthCalculator
as follows:
var calculator = new TotalWorthCalculator(new List<object>()
{
new MoneyWorthCalculator(),
new EtfWorthCalculator()
});
Things to consider:
IAssetWorthCalculator<T>
interface, you might let it inherit from a non-generic IAssetWorthCalculator
interface. Although this makes the implementations slightly more complicated, it prevents the use of dynamic typing. It also allows you to inject the more pleasant IListinto the
TotalWorthCalculator's constructor, compared to the more obscure
IList. This will also simplify composing the
TotalWorthCalculatorusing a DI Container, as they will have trouble resolving an
IList` for you as the type isn't specific.IAssetWorthCalculator<T>.CalculateWorth
method. Also note that calling .CalculateWorth
using dynamic will fail on any type that isn't defined with the public
keyword. I consider this a bug in the C# compiler's runtime code, but is probably a security feature. If you stick with using dynamic typing, make sure to add some unit tests to your project testing the correctness of the TotalWorthCalculator
.IAssetWorthCalculator
isn't suitable, is to use a generic adapter that wraps a generic IAssetWorthCalculator<T>
while implementing a non-generic interface, e.g. ICalculationAdapter { double Calculate(IAssetId id); }
. This will still require some reflection to create new CalculationAdapter<T>
instances, but at least makes the solution safe from Refactoring.