24 votes

IoC (Ninject) et Factories

Si j'ai le code suivant :

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();
    }
  }
}

Ma question est la suivante : quelle est la bonne façon de faire une inversion de contrôle ici ? Je ne veux pas ajouter les concrets KillerRobot et StandardRobot à la classe Factory, n'est-ce pas ? Et je ne veux pas les faire entrer via un IoC.Get<>, n'est-ce pas ? car ce serait du Service Location et non du vrai IoC, n'est-ce pas ? Existe-t-il une meilleure façon d'aborder le problème des changer le béton au moment de l'exécution ?

36voto

Shaun Rowan Points 1348

Pour votre échantillon, vous avez une implémentation de l'usine parfaitement bien et je ne changerais rien.

Cependant, je soupçonne que vos classes KillerRobot et StandardRobot ont en fait des dépendances qui leur sont propres. Je suis d'accord que vous ne voulez pas exposer votre conteneur IoC à la RobotFactory.

Une option consiste à utiliser l'extension d'usine ninject :

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

Il vous donne deux façons d'injecter des usines - par interface, et en injectant un Func qui renvoie un IRobot (ou autre).

Exemple de création d'une usine basée sur une interface : https://github.com/ninject/ninject.extensions.factory/wiki/Factory-interface

Echantillon pour la base func : https://github.com/ninject/ninject.extensions.factory/wiki/Func

Si vous voulez, vous pouvez aussi le faire en liant un func dans votre code d'initialisation IoC. Quelque chose comme :

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);

Votre service de navigation pourrait alors ressembler à ceci :

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

Bien sûr, le problème avec cette approche est que vous écrivez des méthodes d'usine directement dans votre initialisation IoC - peut-être pas le meilleur compromis...

L'extension factory tente de résoudre ce problème en vous proposant plusieurs approches basées sur des conventions - vous permettant ainsi de conserver le chaînage normal des DI avec l'ajout de dépendances contextuelles.

4voto

La façon dont vous devriez le faire :

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

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

Mais de cette façon, je pense que vous perdez le nom null, donc lorsque vous appelez IRobotFactory.Create vous devez vous assurer que le nom correct est envoyé par paramètre.

Lorsque vous utilisez ToFactory() dans la liaison d'interface, tout ce qu'il fait est de créer un proxy en utilisant Castle (ou proxy dynamique) qui reçoit un IResolutionRoot et appelle le Get().

3voto

Tom W Points 1801

Je ne veux pas ajouter les bétons KillerRobot et StandardRobot à la classe Factory, n'est-ce pas ?

Je dirais que vous le faites probablement. Quel serait le but d'une fabrique si ce n'est d'instancier des objets concrets ? Je pense que je peux voir où vous voulez en venir - si IRobot décrit un contrat, le conteneur d'injection ne devrait-il pas être responsable de sa création ? N'est-ce pas à cela que servent les conteneurs ?

Peut-être. Cependant, renvoyer les usines de béton responsables new L'intégration d'objets semble être un modèle assez standard dans le monde IoC. Je ne pense pas qu'il soit contraire au principe d'avoir une usine concrète faisant un travail réel.

0voto

Brian Wells Points 812

Je cherchais un moyen de nettoyer une instruction switch massive qui renvoyait une classe C# pour effectuer un travail (odeur de code ici).

Je ne voulais pas faire correspondre explicitement chaque interface à son implémentation concrète dans le module ninject (essentiellement une imitation du cas de switch long, mais dans un fichier diff), j'ai donc configuré le module pour lier toutes les interfaces automatiquement :

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

Créez ensuite la classe d'usine, en implémentant le StandardKernal qui récupérera les interfaces spécifiées et leurs implémentations via une instance singleton utilisant un 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);
}

Votre implémentation de StandardKernel peut alors accéder à n'importe quelle interface par l'identifiant de votre choix sur l'interface décorant votre classe.

Par exemple :

    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";
    }
}

Utilisation :

        [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);
    }

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X