Выберите тип ковариантного интерфейса в классе, реализующем несколько ковариантных интерфейсов. - PullRequest
1 голос
/ 18 июня 2020

У нас есть простой интерфейс поставщика данных с ковариантным определением параметров.

interface IDataProvider<out T>
{
    T Get();
}

Класс принтера будет печатать значение всех сообщений IMessage.

class Printer
{
    private readonly IEnumerable<IDataProvider<IMessage>> DataProviders;

    public Printer(params IDataProvider<IMessage>[] dataProviders)
    {
        DataProviders = dataProviders;
    }

    public void Print()
    {
        foreach( var dataProvider in DataProviders)
        {
            Console.WriteLine( dataProvider.Get().Message );
        }
    }
}

interface IMessage
{
    string Message { get; }
}

Если все классы реализуют один IDataProvider , поведение такое, как я ожидал. Следующий код напечатает «Hello» и «World».

class Program
{
    static void Main(string[] args)
    {
        HelloProvider helloProvider = new HelloProvider();
        WorldProvider worldProvider = new WorldProvider();
        Printer printer = new Printer(helloProvider, worldProvider);
        printer.Print();
    }
}

class Hello : IMessage
{
    public string Message { get; } = "Hello";
}

class World : IMessage
{
    public string Message { get; } = "World";
}

class HelloProvider : IDataProvider<Hello>
{
    public Hello Get() => new Hello();
}

class WorldProvider : IDataProvider<World>
{
    public World Get() => new World();
}

Однако у нас есть плохой мальчик, реализующий два IDataProvider:

class BadBoy : IDataProvider<Hello>, IDataProvider<World>
{
    // Assume that there are some shared state of Hello and World data.

    Hello IDataProvider<Hello>.Get() => new Hello();

    World IDataProvider<World>.Get() => new World();
}

Я попытался использовать BadBoy, но принтер напечатал два » Привет ".

BadBoy badBoy = new BadBoy();
Printer printer = new Printer( (IDataProvider<Hello>)badBoy, (IDataProvider<World>)badBoy );
printer.Print();

Лучшее решение, которое у нас есть сейчас, - это отдельная реализация IDataProvider:

class HelloProvider : IDataProvider<Hello>
{
    private readonly BadBoy BadBoy;

    public HelloProvider(BadBoy badBoy)
    {
        BadBoy = badBoy;
    }

    public Hello Get() => BadBoy.GetHello();
}

class WorldProvider : IDataProvider<World>
{
    private readonly BadBoy BadBoy;

    public WorldProvider(BadBoy badBoy)
    {
        BadBoy = badBoy;
    }

    public World Get() => BadBoy.GetWorld();
}

class BadBoy
{
    // Assume that there are some shared state of Hello and World data.

    public Hello GetHello() => new Hello();

    public World GetWorld() => new World();
}

Я не удовлетворен этим решением, потому что оно может привести к множеству церемониальных кодов поскольку мы постоянно добавляем новый тип данных в нашу систему. Есть ли лучший способ сообщить классу Printer, какую реализацию IDataProvider он должен использовать?


примечание:

Этот пример представляет собой упрощенную версию нашей реальной проблемы. В нашем проекте мы разрешаем все IDataProvider из контейнера и используем отражение для создания множества классов вычислений с использованием этих IDataProvider. Скажите, пожалуйста, если вам нужна более подробная информация о реальной проблеме.

1 Ответ

2 голосов
/ 18 июня 2020

Вы загнали себя в угол, продублировав контракт. Однако (и не заставляя вас перерисовывать все решение) один подход (хотя и неприятный) заключался бы в том, чтобы отразить интерфейсы с помощью GetInterfaces и GetInterfaceMap, а затем вызвать нужный вам метод, вы потенциально могли бы при необходимости кешируйте MethodInfo.

Возвращает отображение интерфейса для указанного типа интерфейса.

Примечание : Это может помочь вам, а может и нет (в зависимости от ваших обстоятельств) ¯\_(ツ)_/¯

class Printer
{

    public void Print()
    {
       foreach (var dataProvider in DataProviders)
       {
          var dataProviderType = dataProvider.GetType();

          foreach (var type in dataProviderType.GetInterfaces())
          {
             var methodInfo = dataProviderType
                .GetInterfaceMap(type)
                .TargetMethods
                .FirstOrDefault(x => x.Name.EndsWith("Get"));

             var invoke = (IMessage) methodInfo.Invoke(dataProvider, null);
             Console.WriteLine(invoke.Message);
          }
       }
    }

     ...

Использование

BadBoy badBoy = new BadBoy();
Printer printer = new Printer(badBoy);
printer.Print();

Вывод

Hello
World

Обновление

с использованием кеша MethodInfo

public List<MethodInfo> _cache;

public void Print()
{
   foreach (var dataProvider in DataProviders)
   {
      var dataProviderType = dataProvider.GetType();

      if (_cache == null)
      {
         _cache = dataProviderType.GetInterfaces().Select(x => dataProviderType.GetInterfaceMap(x)
            .TargetMethods
            .FirstOrDefault(x => x.Name.EndsWith("Get"))).ToList();
      }


      foreach (var item in _cache)
      {
         var invoke = (IMessage) item.Invoke(dataProvider, null);
         Console.WriteLine(invoke.Message);
      }

   }
}

На моем p c, я могу вызвать оригинал 100000 раз за 400 мс, с кешем, я могу его назвать, 100000 раз за 60 мс.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...