IoC (Ninject) and Factories

2019-01-17 15:45发布

问题:

If I have the following code:

public class RobotNavigationService : IRobotNavigationService {
  public RobotNavigationService(IRobotFactory robotFactory) {
    //...
  }
}
public class RobotFactory : IRobotFactory {
  public IRobot Create(string nameOfRobot) {
    if (name == "Maximilian") {
      return new KillerRobot(); 
    } else {
      return new StandardRobot();
    }
  }
}

My question is what is the proper way to do Inversion of Control here? I don't want to add the KillerRobot and StandardRobot concretes to the Factory class do I? And I don't want to bring them in via a IoC.Get<> right? bc that would be Service Location not true IoC right? Is there a better way to approach the problem of switching the concrete at runtime?

回答1:

For your sample, you have a perfectly fine factory implementation and I wouldn't change anything.

However, I suspect that your KillerRobot and StandardRobot classes actually have dependencies of their own. I agree that you don't want to expose your IoC container to the RobotFactory.

One option is to use the ninject factory extension:

https://github.com/ninject/ninject.extensions.factory/wiki

It gives you two ways to inject factories - by interface, and by injecting a Func which returns an IRobot (or whatever).

Sample for interface based factory creation: https://github.com/ninject/ninject.extensions.factory/wiki/Factory-interface

Sample for func based: https://github.com/ninject/ninject.extensions.factory/wiki/Func

If you wanted, you could also do it by binding a func in your IoC Initialization code. Something like:

var factoryMethod = new Func<string, IRobot>(nameOfRobot =>
                        {
                            if (nameOfRobot == "Maximilian")
                            {
                                return _ninjectKernel.Get<KillerRobot>();
                            }
                            else
                            {
                                return _ninjectKernel.Get<StandardRobot>();
                            }

                        });
_ninjectKernel.Bind<Func<string, IRobot>>().ToConstant(factoryMethod);

Your navigation service could then look like:

    public class RobotNavigationService
    {
        public RobotNavigationService(Func<string, IRobot> robotFactory)
        {
            var killer = robotFactory("Maximilian");
            var standard = robotFactory("");
        }
    }

Of course, the problem with this approach is that you're writing factory methods right inside your IoC Initialization - perhaps not the best tradeoff...

The factory extension attempts to solve this by giving you several convention-based approaches - thus allowing you to retain normal DI chaining with the addition of context-sensitive dependencies.



回答2:

I don't want to add the KillerRobot and StandardRobot concretes to the Factory class do I?

I would suggest that you probably do. What would the purpose of a factory be if not to instantiate concrete objects? I think I can see where you're coming from - if IRobot describes a contract, shouldn't the injection container be responsible for creating it? Isn't that what containers are for?

Perhaps. However, returning concrete factories responsible for newing objects seems to be a pretty standard pattern in the IoC world. I don't think it's against the principle to have a concrete factory doing some actual work.



回答3:

The way you should do:

kernel.Bind<IRobot>().To<KillingRobot>("maximilliam");
kernel.Bind<IRobot>().To<StandardRobot>("standard");
kernel.Bind<IRobotFactory>().ToFactory();

public interface IRobotFactory
{
    IRobot Create(string name);
}

But this way I think you lose the null name, so when calling IRobotFactory.Create you must ensure the correct name is sent via parameter.

When using ToFactory() in interface binding, all it does is create a proxy using Castle (or dynamic proxy) that receives an IResolutionRoot and calls the Get().



回答4:

I was looking for a way to clean up a massive switch statement that returned a C# class to do some work (code smell here).

I didn't want to explicitly map each interface to its concrete implementation in the ninject module (essentially a mimic of lengthy switch case, but in a diff file) so I setup the module to bind all the interfaces automatically:

public class FactoryModule: NinjectModule
{
    public override void Load()
    {
        Kernel.Bind(x => x.FromThisAssembly()
                            .IncludingNonPublicTypes()
                            .SelectAllClasses()
                            .InNamespaceOf<FactoryModule>()
                            .BindAllInterfaces()
                            .Configure(b => b.InSingletonScope()));
    }
}

Then create the factory class, implementing the StandardKernal which will Get the specified interfaces and their implementations via a singleton instance using an IKernal:

    public class CarFactoryKernel : StandardKernel, ICarFactoryKernel{
    public static readonly ICarFactoryKernel _instance = new CarFactoryKernel();

    public static ICarFactoryKernel Instance { get => _instance; }

    private CarFactoryKernel()
    {
        var carFactoryModeule = new List<INinjectModule> { new FactoryModule() };

        Load(carFactoryModeule);
    }

    public ICar GetCarFromFactory(string name)
    {
        var cars = this.GetAll<ICar>();
        foreach (var car in cars)
        {
            if (car.CarModel == name)
            {
                return car;
            }
        }

        return null;
    }
}

public interface ICarFactoryKernel : IKernel
{
    ICar GetCarFromFactory(string name);
}

Then your StandardKernel implementation can get at any interface by the identifier of you choice on the interface decorating your class.

e.g.:

    public interface ICar
{
    string CarModel { get; }
    string Drive { get; }
    string Reverse { get; }
}

public class Lamborghini : ICar
{
    private string _carmodel;
    public string CarModel { get => _carmodel; }
    public string Drive => "Drive the Lamborghini forward!";
    public string Reverse => "Drive the Lamborghini backward!";

    public Lamborghini()
    {
        _carmodel = "Lamborghini";
    }
}

Usage:

        [Test]
    public void TestDependencyInjection()
    {
        var ferrari = CarFactoryKernel.Instance.GetCarFromFactory("Ferrari");
        Assert.That(ferrari, Is.Not.Null);
        Assert.That(ferrari, Is.Not.Null.And.InstanceOf(typeof(Ferrari)));

        Assert.AreEqual("Drive the Ferrari forward!", ferrari.Drive);
        Assert.AreEqual("Drive the Ferrari backward!", ferrari.Reverse);

        var lambo = CarFactoryKernel.Instance.GetCarFromFactory("Lamborghini");
        Assert.That(lambo, Is.Not.Null);
        Assert.That(lambo, Is.Not.Null.And.InstanceOf(typeof(Lamborghini)));

        Assert.AreEqual("Drive the Lamborghini forward!", lambo.Drive);
        Assert.AreEqual("Drive the Lamborghini backward!", lambo.Reverse);
    }