Search code examples
c#ninjectinversion-of-controlioc-containerfactory-pattern

C# - use dependency injection (ninject) instead of factory pattern


I have read a lot about this topic, but couldn't grasp it all the way.

I am trying to use the Ninject.Extensions.Factory instead of my factory to create new objects depending on user input. I want to fully use the Ninject functionality and the IoC concept.

Now the code looks like this:

interface IFeatureFactory
{
    IFeature createFeature(int input);
}

and:

class BasicFeatureFactory : IFeatureFactory
{
    public IFeature createFeature(int input)
    {
        switch (input)
        {
            case 1:
                return new FirstFeature();
            case 2:
                return new SecondFeature();
            default:
                return null;
        }
    }
}

In the future, the IFeature will have dependencies so I want to do it the IoC way.

EDIT:

consumer class - the IFeatureFactory and IUILayer are injected into the FeatureService constructor and resolved using Ninject.

    private IFeatureFactory featureFactory;
    private IUILayer uiHandler;

    public FeatureService(IFeatureFactory featureFactory, IUILayer uiHandler)
    {
        this.featureFactory = featureFactory;
        this.uiHandler = uiHandler;
    }

    public void startService()
    {
        int userSelection = 0;
        uiHandler.displayMenu();
        userSelection = uiHandler.getSelection();
        while (userSelection != 5)
        {
            IFeature feature = featureFactory.createFeature(userSelection);
            if (feature != null)
            {
                IResult result = feature.execFeature();
                uiHandler.displayResult(result);
            }
            else
            {
                uiHandler.displayErrorMessage();
            }
            uiHandler.displayMenu();
            userSelection = uiHandler.getSelection();
        }
    }

and IFeature class:

public interface IFeature
{
    IResult execFeature();
}

Bindings:

    public override void Load()
    {
        Bind<IFeatureFactory>().To<BasicFeatureFactory>();
        Bind<IUILayer>().To<ConsoleUILayer>();
    }

How can I convert this Factory Pattern to IoC with the Ninject.Extensions.Factory? keep in mind the creation of the IFeature is dependent on the user input.


Solution

  • To me it is look like you have 2 options to refactor your code to get the full benefits of ninject.
    The way you're working now is no different than pure di (which there is nothing wrong about it and it is better it some cases) but as you said you want to fully use ninject functionality.

    Option one
    Instead of injecting IFeatureFactory into FeatureService inject the interface IFeatureProvider which will look like this:

    public interface IFeatureProvider
    { 
        IFeature GetFeature(int featureType);
    }
    

    Now your FeatureService will get the requested features from this provider instead of the factory.
    You will need to implement IFeatureProvider and for that you will need 2 more interfaces IFirstFeatureFactory and ISecondFeatureFactory:

    public interface IFirstFeatureFactory
    {
        IFeature CreateFirstFeature();
    }
    
    public interface ISecondFeatureFactory
    {
        IFeature CreateSecondFeature();
    }
    

    And now IFeatureProvider impelementaion:

        public class FeatureProvider: IFeatureProvider
        {
            private readonly IFirstFeatureFactory _firstFeatureFactory;
            private readonly ISecondFeatureFactory _secondFeatureFactory;
    
            public FeatureProvider(IFirstFeatureFactory firstFeatureFactory, ISecondFeatureFactory secondFeatureFactory)
            {
                _firstFeatureFactory=firstFeatureFactory;
                _secondFeatureFactory=secondFeatureFactory;
            }
    
            public IFeautre GetFeature(int featureType)
            {
                switch(featureType)
               {
                     case 1:
                        return _firstFeatureFactory.CreateFirstFeature();
                     case 2:
                        return _secondFeatureFactory.CreateSecondFeature();
                     default:
                        return null;
               }
            }
        }
    

    the thing you should notice that i just extract the object who is responsible for the 'new' into another interface.
    We will not implement the two factory interfaces as ninject will do it for us if we will bind it properly.
    The binding:

    Bind<IFeature>().ToFeature<FirstFeature>().NamedLikeFactoryMethod((IFirstFeatureFactory o) => o.CreateFirstFeature());
    Bind<IFeature>().ToFeature<SecondFeature>().NamedLikeFactoryMethod((ISecondFeatureFactory o) => o.CreateSecondFeature());
    Bind<IFirstFeatureFactory>().ToFactory();
    Bind<ISecondFeatureFactory>().ToFactory();
    Bind<IFeatureProvider>().To<FeatureProivder>();
    

    This 'NameLikeFactoryMethod' binding is equivalent the using of named binding as i did here and it is now the recommended way by ninject for factories.

    The importent thing to notice here you do not implement IFirstFeatureFactory and ISecondFeatureFactory by yourself and you're using ninject functionallity for that.

    The major disadvantage of this option is when we need to add more features we will need to create except for the feature itself another FeatureFactory and also change the FeatureProvider to handle it as well. If the features are not changed very often this option can be good and simple but if they do it can become maintenance nightmare and that is why i suggest option 2.

    Option two
    In this option we will not create any provider class at all and will put all the creation logic inside the factory.
    IFeatureFactory interface will look pretty similar to the one you have now, but instead of using an int as parameter we will use a string (is will suit better with the named binding as we will see soon).

    public interface IFeatureFactory
    {
        IFeature CreateFeature(string featureName);
    }
    

    We will not implement this interfaces by ourselves and let ninject do it for us, however we will need to tell ninject to use the first parameter of CearteFeature to detect which implementation to instantiate(FirstFeatue or SecondFeature).
    For that we will need custom instance provider with this behavior as the StandardInstanceProvider using other convention to choose which implementation to instantiate(the default convention in this article).
    Fortunately ninject show as exactly how we can implement it very quickly by UseFirstArgumentAsNameInstanceProvider.
    Now the binding:

    Bind<IFeature>().To<FirstFeature>().Named("FirstFeature");
    Bind<IFeature>().To<FirstFeature>().Named("SecondFeature");
    Bind<IFeatureFactory>().ToFactory(() => new UseFirstArgumentAsNameInstanceProvider());
    

    The things to notice here:

    • We will not implement the factory interface by ourselves.
    • We are using named binding for each implementation and we will get the implementation from the factory according to the featureName we will pass to the factory method (this is why i prefer the parameter to be string).
      Notice that if you pass feature name that is not "FirstFeature" or "SecondFeature" exception will be thrown.
    • We are using UseFirstArgumentAsNameInstanceProvider as our instance provider for our factory as mentioned before.

    This has solved the problem in the first option as if we want to add new feature we well just need to create it and bind it to his interface with his name.
    Now the client of the factory can ask from the factory feature with this new name without the need of changes in other classes.

    Conslucsion
    By choosing one of this options above over pure di we will get the benefits of letting ninject kernel create our object in the factories instead of doing all the 'new' on our own.
    Although there is nothing wrong about not using IoC container this can really helps us in cases there is a big dependencies graph inside IFeature implementations as ninject will inject them for us. By doing one of this options we are fully using ninject functionality without using a 'Service locator' which is considered as an anti pattern.