0

I'm trying to implement a system that allows to read and interpret lines from a file.

I need to manage different file formats. For that I have an abstract Importer class that is is inherited and implemented differently (based on the file format).

The lines of a file can result in different object, so I created a generic interface that know how to parse the line, validate it, etc: public interface ILineImporter<ObjType> where ObjType : IImportableObject

The concrete Importer class knows which LineImporter to use for a given line, via the overriden abstract method public abstract ILineImporter<IImportableObject> GetLineImporter(string line);.

The problem is that in the implementation of this method, the returned type depends of the concrete Importer and line:

public override ILineImporter<IImportableObject> GetLineImporter(string line)
{
    // TODO: Return the appropriate LineImporter for the given line
    // For the example, I always return a MyObjectALineImporter, but it can potentially be any ILineImporter<IImportableObject>
    return new MyObjectALineImporter();
}

That doesn't compile, because the compiler can't implicitly convert MyObjectALineImporter to ILineImporter<IImportableObject>.

If I add the in or out keywords to use covariance/contravariance, the compiler indicates that my generic interface is not covariantly/contravariantly valid.

Here is the simplified source code that you can throw in a standard Console app to reproduce the issue:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace ConsoleApplication2
{
    public interface IImportableObject { }
    public class MyObjectA : IImportableObject { }

    public interface ILineImporter<ObjType> where ObjType : IImportableObject
    {
        ObjType GetObject();
        bool IsObjectValid(ObjType o);
    }

    /// <summary>
    /// Concrete class that knows how to get the appropriate MyObjectA instance, validate it, etc.
    /// </summary>
    public class MyObjectALineImporter : ILineImporter<MyObjectA>
    {
        public MyObjectA GetObject()
        {
            Console.WriteLine("GetObject");
            return new MyObjectA(); // For the example, I create a new instance but this method can potentially return an existing object from DB.
        }

        public bool IsObjectValid(MyObjectA o)
        {
            Console.WriteLine("IsValid");
            // TODO : Test if the object is valid
            return true;
        }
    }

    public abstract class Importer
    {
        public abstract ILineImporter<IImportableObject> GetLineImporter(string line);
        public void Importe(string text)
        {
            using (StringReader reader = new StringReader(text))
            {
                string line;
                while ((line = reader.ReadLine()) != null)
                {
                    var lineImporter = this.GetLineImporter(line);
                    var obj = lineImporter.GetObject();

                    bool isValid = lineImporter.IsObjectValid(obj);
                }
            }
        }
    }

    public class ConcreteImporter1 : Importer
    {
        public override ILineImporter<IImportableObject> GetLineImporter(string line)
        {
            // TODO: Return the appropriate LineImporter for the given line
            // For the example, I always return a MyObjectALineImporter, but it can potentially be another ILineImporter
            return new MyObjectALineImporter();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Importer importer = new ConcreteImporter1(); // TODO : Retrieve the appropriate Importer with a Factory.
            importer.Importe(string.Empty);

            Console.ReadKey();
        }
    }
}

What would be the right way to handle this problem?

Olivier Payen
  • 15,198
  • 7
  • 41
  • 70

2 Answers2

1

Because the interface do get ObjType as a parameter (of a method), and returns it (from another method), i cannot be covariant, nor contravariant.

I suggest you create a non-generic ILineImporter interface, which it's methods works with IImportableObject, which the generic interface would extand/inherit from.

Then MyObjectALineImporter would be able to be casted to 'ILineImporter` (the non-generic one).

public interface ILineImporter
{
    IImportableObject GetObject();
    bool IsObjectValid(IImportableObject o);
}

public interface ILineImporter<ObjType> : ILineImporter
    where ObjType : IImportableObject
{
    ObjType GetObject();
    bool IsObjectValid(ObjType o);
}
Shlomi Borovitz
  • 1,700
  • 9
  • 9
  • Now it says that 'MyObjectALineImporter.GetObject()' cannot implement 'ILineImporter.GetObject()' because it does not have the matching return type of 'IImportableObject'. That's weird, since its parent class (ILineImporter) has a type constraint of IImportableObject – Olivier Payen Mar 04 '14 at 09:35
  • You should use explicit interface implementation for the more basic interface. [How to use explicit interface implementation](http://msdn.microsoft.com/en-us/library/ms173157.aspx) – Shlomi Borovitz Mar 04 '14 at 09:46
  • Thanks, now it compiles. However it forces me to duplicate each method in my concrete LineImporters (one for the non-generic interface and one for the generic), and it forces me to cast the IImportableObject parameter received on the methods to the concrete class (I lose the advantages of generics). Any idea? – Olivier Payen Mar 04 '14 at 10:08
  • About the method duplication - unfortunately it is a requirement, but a small one (the none generic implementation should just call the generic one. And about the casting of the importer - you're not loosing the advantages of generics, because 2 things: 1. In your initial implementation, you tried to store the line importer as `ILineImporter`, which is exactly like the non-generic `ILineImporter` (and that's OK, as the importer doesn't need to now the exact line importer in use). 2. in the next comment -> – Shlomi Borovitz Mar 04 '14 at 10:59
  • 2. You need to do that cast in exactly 1 place: the non-generic `IsObjectValid` method (and then, just call the generic one, from the non-generic). I think that cast is a small sacrifice to gain polymorphism. – Shlomi Borovitz Mar 04 '14 at 11:00
  • 1
    I simplified my code to post it here, so actually there are more than 2 methods in my LineImporters. What I ended up doing to avoid code duplication and unnecessary casting methods is add a generic abstract parent LineImporter class that explicitly implements the 2 interfaces and calls generic abstracts methods. – Olivier Payen Mar 04 '14 at 13:31
0

You could add an invariant interface that Importer inherits from:

public interface ISomeInterface<TObject> where TObject : IImportableObject {
    ILineImporter<TObject> GetLineImporter(string line);
}
public abstract class Importer<T> : ISomeInterface<T> where T: IImportableObject
{
   public abstract ILineImporter<T> GetLineImporter(string line);
   public void Importe(string text)
   {
       using (StringReader reader = new StringReader(text))
       {
           string line;
           while ((line = reader.ReadLine()) != null)
           {
               var lineImporter = this.GetLineImporter(line);
               var obj = lineImporter.GetObject();

               bool isValid = lineImporter.IsObjectValid(obj);
           }
       }
   }
}

Your ConcreteImporter1 then inherits a type parameterized Importer:

public class ConcreteImporter1 : Importer<MyObjectA>
{
   public override ILineImporter<MyObjectA> GetLineImporter(string line)
   {
       return new MyObjectALineImporter();
   }
}

Note that ISomeInterface is invariant, ie both co- and contravariant, so there is neither in nor out in the definition.

carlpett
  • 12,203
  • 5
  • 48
  • 82
  • I can't do that, because my Importer concrete implementations can return differents LineImporter based on the 'line' argument. The Importer returns the appropriate LineImporter for a given line. – Olivier Payen Mar 04 '14 at 09:39
  • @OlivierPayen: In that case you'll need to go with a non-generic "base" interface - `I` isn't assignable to `I`. Does that break some other part of your design? – carlpett Mar 04 '14 at 09:42
  • I think that this is what recommended Shlomi Borovitz in his answer, but I still have a problem with that (read the comment on his answer). – Olivier Payen Mar 04 '14 at 09:47