One Interface, Multiple Implementations

Feb 21, 2014 at 2:01 AM
Hello,
I found this older thread, and I wanted to follow up with my own example to make sure I understand the advice.

I've got an interface called ISpreadsheetImporter, which would have two different implementations, say SpreadsheetImporterTypeA and TypeB
public interface ISpreadsheetImporter
{
    void Import();
}
public class SpreadsheetImporterTypeA : ISpreadsheetImporter
public class SpreadsheetImporterTypeB : ISpreadsheetImporter
Different controllers in my MVC app would need either TypeA or TypeB. What would be the best way for me to register these two implementations to the one interface? RegisterAll()?

And, more importantly, should I be designing it like this in the first place? Would it be better to have an interface for each concrete implementation? But then isn't it a little strange to have an interface with only one implementation? Why have the abstraction in that case?

To get to the heart of it, I guess I'm really trying to sharpen my design skills rather than just learn the method I need and call it a day. Of course, in the end I'd like know how I can get SI to do what I need, but I'd be really interested in how you approach such problems.

Thanks!
Coordinator
Feb 21, 2014 at 5:24 AM
How do those implementations differ? What do they do? Can you say more about this?
Feb 21, 2014 at 6:34 AM
Edited Feb 21, 2014 at 6:38 AM
I'm also having a hard time figuring this approach out. For now I'm doing it like this:
container.RegisterAll<IMyService>(typeof(ConcreteService1), typeof(ConcreteService2), typeof(ConcreteService3));
If one of your services contains a constructor, which cannot be automaticly injected, you can define the registration, with a delegate, like this:
container.RegisterSingle<ConcreteService1>(() => new ConcreteService(param1, param2, etc);
I then resolve my IMyServices with container.GetAllInstances<IMyService>() shown below:
container.RegisterSingle<IAnotherService>(() =>
         new AnotherService(container.GetAllInstances<IMyService>()
                   .SingleOrDefault(x => x.GetType() == typeof(ConcreteService1)),
            container.GetInstance<IAnotherDependency>()));
By this approach you loose the possiblity of automatic constructor injection, which is the preferred way of registering types.

In my case my three concrete instances of my IMyService interface varies by some configuration specified in the database. Not sure if this would be better handled with keyed registrations, which SimpleInjector doesn't support by default.
Feb 21, 2014 at 2:11 PM
dot_NET_Junkie wrote:
How do those implementations differ? What do they do? Can you say more about this?
Depending on the type of spreadsheet being uploaded, I would need to parse it into different model objects and apply different validation rules. With the design as it is now, the implementation classes (type A or B) would verify that they were given the correct spreadsheet type, parse the spreadsheet into objects, and apply validation.

I'm concerned that I may be overstepping SRP with this design, so I've thought about having an interface for a validation class as well, but first I've been trying to get a good mental handle on how these interface would work together with DI.

Thanks!
Coordinator
Feb 21, 2014 at 3:16 PM
Kyle, my first impression is still that those implementations (A and B) are not an implementation of the same abstraction. You can find out by swapping those implementations, so instead of doing new ControllerA(new SpreadsheetImporterTypeA()) and new ControllerB(new SpreadsheetImporterTypeB()), you do new ControllerB(new SpreadsheetImporterTypeA()) and new ControllerA(new SpreadsheetImporterTypeB()). If the system still functionally behaves correctly, in that case those implementations are in fact implementations of the same abstraction. If this breaks your application, you should consider them implementations of different abstractions. They are different services.

If they are different services, the solution is of course simple: define an IASpreadsheetImporter and IBSpreadsheetImporter, let the controllers reference either one of them, and you're done.

If they are two interchangeable implementations of the same service, there are a few things you can do. For instance, you can create an ISpreadsheetImporterFactory and a controller can request the importer it needs. For instance var importer = this.spreadsheetImporterFactort.Create(ImporterType.A). This of course moves the decision to the controller logic, which might not be what you want.

So another option is to use Context Based Injection. You can copy-paste the RegisterWithContext extension method from the documentation and this allows you to do this:
container.RegisterWithContext<ISpreadsheetImporter>(context =>
{
    if (context.ImplementationType == typeof(ControllerA)
        container.GetInstance<SpreadsheetImporterTypeA>();
    else
        container.GetInstance<SpreadsheetImporterTypeB>();
});
Marked as answer by dot_NET_Junkie on 2/26/2014 at 1:13 PM
Feb 21, 2014 at 3:27 PM
I think the conclusion in my case has to be that the implementations do not implement the same abstraction, since I could not interchange the two according to the experiment you suggested.

In that case, I'll go ahead and create an interface for each implementation, which will make the registration very simple. Your suggestions are very nice, though, and I will definitely refer back to them if I do ever encounter this problem again.

Thanks for your time!
Coordinator
Feb 22, 2014 at 7:30 AM
Edited Feb 22, 2014 at 7:35 AM
I find the simplest way to achieve this requirement is to use decorators and have them function as a chain of responsibility.
public interface ISpreadsheetImporter
{
    void Import(SpreadSheet spreadSheet);
}

public class SpreadSheet
{
    public string Type { get; set; }
    public string ProcessedBy { get; set; }
}
Each decorator decides whether to process the spreadsheet itself or hand-off to the next implementation:
private class SpreadsheetImporterTypeA : ISpreadsheetImporter
{
    public void Import(SpreadSheet spreadSheet)
    {
        spreadSheet.ProcessedBy = this.GetType().Name;
    }
}

private class SpreadsheetImporterTypeB : ISpreadsheetImporter
{
    private readonly ISpreadsheetImporter decorated;
    public SpreadsheetImporterTypeB(ISpreadsheetImporter decorated)
    {
        this.decorated = decorated;
    }

    public void Import(SpreadSheet spreadSheet)
    {
        if (spreadSheet.Type == "B")
        {
            spreadSheet.ProcessedBy = this.GetType().Name;
        }
        else
        {
            this.decorated.Import(spreadSheet);
        }
    }
}
[Test]
public void SpreadSheet_TypeA_IsProcessedByTypeAImporter()
{
    var container = new Container();
    container.Register<ISpreadsheetImporter, SpreadsheetImporterTypeA>();
    container.RegisterDecorator(
        typeof(ISpreadsheetImporter),
        typeof(SpreadsheetImporterTypeB));

    var importer = container.GetInstance<ISpreadsheetImporter>();
    var spreadSheet = new SpreadSheet() { Type = "A" };

    importer.Import(spreadSheet);

    Assert.That(
        spreadSheet.ProcessedBy,
        Is.EqualTo(typeof(SpreadsheetImporterTypeA).Name));
}

[Test]
public void SpreadSheet_TypeB_IsProcessedByTypeBImporter()
{
    var container = new Container();
    container.Register<ISpreadsheetImporter, SpreadsheetImporterTypeA>();
    container.RegisterDecorator(
        typeof(ISpreadsheetImporter),
        typeof(SpreadsheetImporterTypeB));

    var importer = container.GetInstance<ISpreadsheetImporter>();
    var spreadSheet = new SpreadSheet() { Type = "B" };

    importer.Import(spreadSheet);

    Assert.That(
        spreadSheet.ProcessedBy,
        Is.EqualTo(typeof(SpreadsheetImporterTypeB).Name));
}