Получение универсального класса, назначенного данному типу объекта

Я пишу программу на 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;
    }
}

Я пытаюсь найти способ реализовать класс, который возвращает правильный калькулятор стоимости для данного идентификатора актива, например.

  • если идентификатор актива является валютой, класс возвращает объект MoneyWorthCalculator.
  • если идентификатор актива — ETF, он возвращает EtfWorthCalculator

Проблема 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;
    }
}

Что плохого в том, чтобы просто проверить тип в цикле foreach и посчитать стоимость отдельно? Вы не ограничены одной переменной типа IAssetWorthCalculator<IAssetId>. Создайте MoneyWorthCalculator и EtfWorthCalculator, отметьте id is ... и используйте правильный калькулятор.

Sweeper 26.08.2024 13:51

очень неясно, как бы вы произвели расчеты стоимости утверждения на основе строки, если честно... может быть, более полный пример?

Ivan Petrov 26.08.2024 18:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
50
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Я думаю, вам нужно прочитать о 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, но, по моему мнению, это не должно вызвать особых проблем.

Другие вопросы по теме