2

I'm still new to using Autofac and I'm bothered by the constructor injection method that I'm using. Here's the scenario:

I currently have two classes which inherits the IForms interface. Each of the classes has its own interface as well

public interface IForms 
{
    long CreateNewForm(FormData data);
    FormData RetrieveFormById(long id);
}

public interface IFormA : IForms
{ }

public interface IFormB : IForms
{ }

Now I have a class which handles this which is like this:

public class ApplicationForms : IApplicationForms
{
    private readonly IFormA _formA;
    private readonly IFormB _formB;

    public ApplicationForms(IFormA formA, IFormB formB)
    {
        _formA = formA;
        _formB = formB;
    }

    public void SubmitApplicationForm(FormData data)
    {
        switch(data.FormType)
        {
            case FormType.FormA:
                _formA.CreateNewForm(data);
                break;
            case FormType.FormB:
                _formB.CreateNewForm(data);
                break;
        }
    }
}

Now there's a possibility that there will be 2 more forms coming in (ex. FormC, FormD, FormE). What will happen here is that there will be 3 more constructor parameters in the ApplicationForms constructor.

Is there a way to like combine all the constructor parameters into one parameter? I can see that it will definitely look ugly in the end.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Musikero31
  • 3,115
  • 6
  • 39
  • 60

2 Answers2

2

Since there is a common IForms interface, then you can inject an enumeration.

This looks like a good candidate for strategy pattern.

I would suggest refactoring the base interface to be able to identify what type of form it is

public interface IForms {
    FormType FormType { get; }
    long CreateNewForm(FormData data);
    FormData RetrieveFormById(long id);
}

and update the dependent class

public class ApplicationForms : IApplicationForms {
    private readonly IEnumerable<IForms> forms;

    public ApplicationForms(IEnumerable<IForms> forms) {
        this.forms = forms;
    }

    public void SubmitApplicationForm(FormData data) {
        var form = forms.FirstOrDefault(_ => _.FormType == data.FormType);
        if(form != null)
            form.CreateNewForm(data);

        //...
    }
}

This assumes that when registering the derived interfaces that you associate it with the base IForms interface.

var builder = new ContainerBuilder();
builder.RegisterType<FormA>().As<IForms>();
builder.RegisterType<FormB>().As<IForms>();
builder.RegisterType<FormC>().As<IForms>();

//...

Now no matter how many more forms are added the class can perform without modification.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
2

The problem you're describing is that you have many forms, but at runtime you need one specific form, and so you don't want to inject all of the forms. That might be a good scenario for an abstract factory.

We often represent the factory as an interface with a single method, but we can also do it with a delegate:

public delegate IForm GetFormByTypeFunction(FormType formType);

Now your class looks like this:

public class ApplicationForms : IApplicationForms
{
    private readonly GetFormByTypeFunction _getFormByType;

    public ApplicationForms(GetFormByTypeFunction getFormByType)
    {
        _getFormByType = getFormByType;
    }

    public void SubmitApplicationForm(FormData data)
    {
        var form = _getFormByType(data.FormType);
        form.CreateNewForm(data);
    }
}

Now the question is how to implement the factory. It might still have a switch statement or something that doesn't seem too elegant, but that's okay. The point of the factory is that however it works, the business of creating and/or selecting an implementation is moved out of the class that depends on the implementation.

You can register a delegate with Autofac like this:

builder.Register<GetFormByTypeFunction>(context => formType =>
{
    switch (formType)
    {
        case FormType.Type1:
        {
            return context.Resolve<FormOne>();
        }
        case FormType.Type2:
        {
            return context.Resolve<FormTwo>();
        }
        default:
            throw new InvalidOperationException("Unknown form type");
    }
});

Now you don't need to resolve all of the IForm implementations up front because you can resolve the one you want directly from the container once you know which one you want.

That might seem "wrong" because you're resolving from the container. But you're not resolving directly from the container. You're depending on a factory. That factory can be replaced with any other implementation which means that your class does not depend on the container.

This sort of factory is also super easy to mock. Technically it's not even a mock. It's just an implementation of the factory that returns a mock.

var formMock = new Mock<IForm>();
var factory = new GetFormByTypeFunction(formType => formMock.Object);
Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
  • I like the factory delegate idea. Saves having to initialize all the implementations. nice. – Nkosi Jun 03 '19 at 14:30
  • Will there be any performance hit or any implication if I inject all forms? I would like to understand that as well. – Musikero31 Jun 03 '19 at 14:45
  • Injecting all of them if you only need one might be less efficient, but that's minimized if new instances aren't getting created each time. Even if you are creating new instances I wouldn't worry much about performance. If you inject them all, the implication is that your class depends on them all. But you don't need them all - you need one based on an argument. It's clearer if you model the class after what it needs and does. Also, if another class needs the same behavior you don't want to duplicate the logic. – Scott Hannen Jun 03 '19 at 14:51
  • @Musikero31 I would suggest this approach over resolving all the implementations for injection if all you really need is one of them based on a condition. – Nkosi Jun 03 '19 at 15:08
  • Even if the `FormType` is coming from the client? Also, the registration is from the Global.asax. So I'm not sure how it will turn out. – Musikero31 Jun 04 '19 at 02:41
  • @ScottHannen I understood this when I bumped into this link http://benedict-chan.github.io/blog/2014/08/13/resolving-implementations-at-runtime-in-autofac/ I'll be using this answer. Thank you! – Musikero31 Jun 07 '19 at 03:30