Replace factory with AutoFac
Asked Answered
T

1

17

I'm accustomed to creating my own factories as shown (this is simplified for illustration):

public class ElementFactory
{
    public IElement Create(IHtml dom)
    {
        switch (dom.ElementType)
        {
            case "table":
                return new TableElement(dom);
            case "div":
                return new DivElement(dom);
            case "span":
                return new SpanElement(dom);
        }
        return new PassthroughElement(dom);
    }
}

I'm finally getting around to using an IoC container (AutoFac) in my current project, and I'm wondering is there some magic way of achieving the same thing elegantly with AutoFac?

Teahan answered 21/8, 2013 at 20:11 Comment(3)
What's the advantage of replacing this factory with Autofac?Jennie
Sorry, as I alluded to in my comment on KeithS's answer, I was hoping there'd be some AutoFac voodoo that let me avoid the switch. What's the advantage of avoiding the switch? I dunno... I just seem to have been indoctrinated with an aversion to them :(Teahan
IoC is no magic wand. You can use named registrations, and you might remove the switch, but in the end there will always be some sort of switching logic. My advice: use a factory fort his and register the factory in Autofac.Jennie
S
25

Short answer: Yes.

Longer answer: First, in simple cases where a class Foo is registered as the implementation for IFoo, constructor parameters or properties of type Func<IFoo> will be resolved automatically by Autofac, with no additional wiring needed. Autofac will inject a delegate that basically executes container.Resolve<IFoo>() when invoked.

In more complex cases like yours, where the exact concretion returned is based on input parameters, you can do one of two things. First, you can register a factory method as its return value to provide a parameterized resolution:

builder.Register<IElement>((c, p) => {
    var dom= p.Named<IHtml>("dom");
    switch (dom.ElementType)
    {
        case "table":
            return new TableElement(dom);
        case "div":
            return new DivElement(dom);
        case "span":
            return new SpanElement(dom);
    }
    return new PassthroughElement(dom);
  });

//usage
container.Resolve<IElement>(new NamedParameter("dom", domInstance))

This isn't type-safe (domInstance won't be compiler-checked to ensure it's an IHtml), nor very clean. Instead, another solution is to actually register the factory method as a Func:

builder.Register<Func<IHtml, IElement>>(dom =>
{
    switch (dom.ElementType)
    {
        case "table":
            return new TableElement(dom);
        case "div":
            return new DivElement(dom);
        case "span":
            return new SpanElement(dom);
    }
    return new PassthroughElement(dom);
});


public class NeedsAnElementFactory //also registered in AutoFac
{
    protected Func<IHtml,IElement> CreateElement {get; private set;}

    //AutoFac will constructor-inject the Func you registered
    //whenever this class is resolved.
    public NeedsAnElementFactory(Func<IHtml,IElement> elementFactory)
    {
        CreateElement = elementFactory;
    }  

    public void MethodUsingElementFactory()
    {
        IHtml domInstance = GetTheDOM();

        var element = CreateElement(domInstance);

        //the next line won't compile;
        //the factory method is strongly typed to IHtml
        var element2 = CreateElement("foo");
    }
}

If you wanted to keep the code in ElementFactory instead of putting it in the Autofac module, you could make the factory method static and register that (this works especially well in your case because your factory method is trivially made static):

public class ElementFactory
{
    public static IElement Create(IHtml dom)
    {
        switch (dom.ElementType)
        {
            case "table":
                return new TableElement(dom);
            case "div":
                return new DivElement(dom);
            case "span":
                return new SpanElement(dom);
        }
        return new PassthroughElement(dom);
    }
}

...

builder.Register<Func<IHtml, IElement>>(ElementFactory.Create);
Sejant answered 21/8, 2013 at 20:56 Comment(4)
Thanks, that's really useful! I was kind of hoping AutoFac would help me avoid the ugly switch statement, but there it was, staring at me, practically mocking me, in all three of your code blocks :)Teahan
Well it's also possible to use "keyed registrations", specifying each of the element type names as a key for a resolution method for the corresponding object type. The complication in this situation is that not all possible return types are associated with a key (you have a default case), and the IHtml parent of the element name is needed for other reasons. So, it seemed easiest just to take your working code and set it up so AutoFac could use it.Sejant
The registration does not compile: builder.Register<Func<IHtml, IElement>>(ElementFactory.Create); I would expect it to look somewhat like this: builder.Register<Func<IHtml, IElement>>((context, parameters) => (html => ElementFactory.Create(html)));Bragg
Or even simpler: context => builder.Register<Func<IHtml, IElement>>(context => ElementFactory.Create);Bragg

© 2022 - 2024 — McMap. All rights reserved.