简体   繁体   中英

Select the type of a covariant interface in a class that implement multiple covariant interfaces

We have a simple data provider interface with covariant parameter definition.

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

The printer class will print the value of all 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; }
}

If all class implement one IDataProvider, the behavior is as my expectation. Following code will print "Hello" and "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();
}

However we have a bad boy implement two 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();
}

I tried to cast BadBoy but Printer print two "Hello".

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

The best solution we have now is separate IDataProvider implementation:

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();
}

I'm not satisfied with this solution because it may result in a lot of ceremonial codes as we continuously add new data type to our system. Is there better way to tell Printer class which implementation of IDataProvider it should use?


note:

This example is a simplified version of our real problem. In our project we resolve all IDataProvider from container and use reflection to create many calculation classes using those IDataProvider. Please tell me if you need more detail of the real problem.

You have painted yourself into a corner by duplicating a contract. However (and without making you redsign the whole solution), one approach (albeit nasty) would be to reflect out the interfaces by using GetInterfaces and GetInterfaceMap then invoking the method you need, you could potentially cache the MethodInfo, if need be.

Returns an interface mapping for the specified interface type.

Note : This may or may not be helpful to you (depending on your circumstances) ¯\_(ツ)_/¯

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);
          }
       }
    }

     ...

Usage

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

Output

Hello
World

Update

using a MethodInfo cache

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);
      }

   }
}

On my pc, i can call the original 100,000 times in 400ms, with the cache, i can call it, 100,000 times in 60ms.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM