Я пишу программу на C#, которая рассчитывает общую стоимость финансовых активов. Каждый актив представлен идентификатором актива. Существуют классы калькуляторов, специализирующиеся на расчете стоимости определенного идентификатора актива.
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)
{
...
}
}
Теперь предположим, что у меня есть список различных идентификаторов активов, для которых я хочу рассчитать общую стоимость.
List<IAssetId> assetIds = [
new Currency("USD"),
new EtfTicker("SPY")
];
decimal worth = TotalWorthCalculator.GetTotalWorth(assetIds);
Расчеты общей стоимости будут выполняться в классе, который будет выглядеть примерно так:
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;
}
}
Я пытаюсь найти способ реализовать класс, который возвращает правильный калькулятор стоимости для данного идентификатора актива, например.
Проблема 1: Я не могу получить общий тип для калькуляторов стоимости (чтобы использовать его в классе, который возвращает правильные калькуляторы для данного идентификатора актива). Поэтому попытка сделать следующее вызывает ошибку компилятора (поскольку IAssetId является более базовым типом, чем Currency в MoneyWorthCalculator):
IAssetWorthCalculator<IAssetId> worthCalculator = new MoneyWorthCalculator();
Проблема 2: Каждый калькулятор имеет метод CalculateWorth, который принимает только определенный тип параметра, поэтому MoneyWorthCalculator не принимает IAssetId.
Я думал об использовании неуниверсальных версий калькуляторов стоимости, но это потребовало бы проверки типа параметра метода, что нарушило бы принцип подстановки Лискова:
public class MoneyWorthCalculator : IAssetWorthCalculator
{
public decimal CalculateWorth(IAssetId assetId)
{
if (assetId is not Currency)
{
throw new ArgumentException();
}
return 10;
}
}
очень неясно, как бы вы произвели расчеты стоимости утверждения на основе строки, если честно... может быть, более полный пример?
Я думаю, вам нужно прочитать о Factory Pattern. Он поможет вам заменить класс в зависимости от валюты и актива.
Не внося изменений в определенные абстракции, вы можете изменить TotalWorthCalculator
на следующее:
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);
}
}
В этом решении используется отражение и динамическая типизация. Вы можете создать экземпляр TotalWorthCalculator
следующим образом:
var calculator = new TotalWorthCalculator(new List<object>()
{
new MoneyWorthCalculator(),
new EtfWorthCalculator()
});
Что ж, если вы используете контравариантность с ключевым словом in
, вы фактически говорите, что для IAssetWorthCalculator
не допускается подкласс.
Таким образом, даже если бы у вас был способ сделать то, что вам нужно, в конечном итоге вы получили бы ошибки.
Этот код не будет соответствовать:
AssetWorthCalculator<IAssetId> generic = null;
AssetWorthCalculator<Currency> lessGeneric = null;
generic = lessGeneric;
с ошибкой:
Невозможно неявно преобразовать тип «console.AssetWorthCalculator<console.Currency>» в «console.AssetWorthCalculator<console.IAssetId>». Существует явное преобразование (вам не хватает приведения?)
в то время как lessGeneric = generic
было бы прекрасно.
Поэтому вам нужно переосмыслить свой дизайн.
Вам нужна ковариация, но вы используете параметр типа для аргументации, для которой требуется контравариация.
Я думаю, вам нужен тип интерфейса для List<T>
, например:
public interface IAmAnAssetAndIKnowMyWorth {
decimal CalculateWorth();
}
и общий класс, который его реализует:
public class GenericKnowsItsWorth<TAssetId>(
TAssetId assetId, ICalculator<TAssetId> calculator) :
IAmAnAssetAndIKnowMyWorth
where TAssetId : IAssetId {
public decimal CalculateWorth() => calculator.CalculateWorth(assetId);
}
в который вы можете внедрить различные калькуляторы, реализующие
public interface ICalculator<T> where T : IAssetId {
public decimal CalculateWorth(T assetId);
}
за T
быть Currency
например
public class MoneyWorthCalculator : ICalculator<Currency> {
public decimal CalculateWorth(Currency assetId) {
return default;
}
}
мы можем создавать такие экземпляры:
IAmAnAssetAndIKnowMyWorth currencyKnowsItsWorth = new GenericKnowsItsWorth<Currency>(
new Currency("EUR"),
new MoneyWorthCalculator());
это облегчит задачу в конце:
public decimal GetTotalWorth(IList<IAmAnAssetAndIKnowMyWorth assets)
{
decimal sum = 0;
foreach (IAmAnAssetAndIKnowMyWorth asset in assets)
{
sum += asset.CalculateWorth();
}
return sum;
}
Это приведет к тому, что потребуется немного больше работы по построению IList<IAmAnAssetAndIKnowMyWorth>
с объединением AssetIds
и Calculators
, но, по моему мнению, это не должно вызвать особых проблем.
Что плохого в том, чтобы просто проверить тип в цикле
foreach
и посчитать стоимость отдельно? Вы не ограничены одной переменной типаIAssetWorthCalculator<IAssetId>
. СоздайтеMoneyWorthCalculator
иEtfWorthCalculator
, отметьтеid is ...
и используйте правильный калькулятор.