web-dev-qa-db-fra.com

Méthode d'usine avec DI et IoC

Je connais bien ces modèles, mais je ne sais toujours pas comment gérer la situation suivante:

public class CarFactory
{
     public CarFactory(Dep1,Dep2,Dep3,Dep4,Dep5,Dep6)
     {
     }

     public ICar CreateCar(type)
     {
            switch(type)
            {
               case A:
                   return new Car1(Dep1,Dep2,Dep3);
               break;

               case B:
                   return new Car2(Dep4,Dep5,Dep6);
               break;
            }
     }
}

En général, le problème réside dans la quantité de références à injecter. Ce sera encore pire quand il y aura plus de voitures.

La première approche qui me vienne à l’esprit est d’injecter Car1 et Car2 dans le constructeur de l’usine, mais c’est à l’encontre de celle-ci car l’usine retournera toujours le même objet. La deuxième approche consiste à injecter un servicelocator mais c'est antipattern partout. Comment le résoudre?

Modifier:

Voie alternative 1:

public class CarFactory
{
     public CarFactory(IContainer container)
     {
        _container = container;
     }

     public ICar CreateCar(type)
     {
            switch(type)
            {
               case A:
                   return _container.Resolve<ICar1>();
               break;

               case B:
                     return _container.Resolve<ICar2>();
               break;
            }
     }
}

Autre manière 2 (trop difficile à utiliser à cause d'un trop grand nombre de dépendances dans l'arbre):

public class CarFactory
{
     public CarFactory()
     {
     }

     public ICar CreateCar(type)
     {
            switch(type)
            {
               case A:
                   return new Car1(new Dep1(),new Dep2(new Dep683(),new Dep684()),....)
               break;

               case B:
                    return new Car2(new Dep4(),new Dep5(new Dep777(),new Dep684()),....)
               break;
            }
     }
}
31
MistyK

Avoir une déclaration de cas de commutateur à l'intérieur d'une usine est une odeur de code. Fait intéressant, vous ne semblez pas vous concentrer sur la résolution de ce problème.

La meilleure solution, la plus adaptée aux DI pour ce scénario, est le modèle strategy . Il permet à votre conteneur DI d'injecter les dépendances dans les instances d'usine auxquelles elles appartiennent, sans encombrer les autres classes contenant ces dépendances ni recourir à un localisateur de service.

Des interfaces

public interface ICarFactory
{
    ICar CreateCar();
    bool AppliesTo(Type type);
}

public interface ICarStrategy
{
    ICar CreateCar(Type type);
}

Des usines

public class Car1Factory : ICarFactory
{
    private readonly IDep1 dep1;
    private readonly IDep2 dep2;
    private readonly IDep3 dep3;

    public Car1Factory(IDep1 dep1, IDep2 dep2, IDep3 dep3)
    {
        if (dep1 == null)
            throw new ArgumentNullException("dep1");
        if (dep2 == null)
            throw new ArgumentNullException("dep2");
        if (dep3 == null)
            throw new ArgumentNullException("dep3");

        this.dep1 = dep1;
        this.dep2 = dep2;
        this.dep3 = dep3;
    }

    public ICar CreateCar()
    {
        return new Car1(this.dep1, this.dep2, this.dep3);
    }

    public bool AppliesTo(Type type)
    {
        return typeof(Car1).Equals(type);
    }
}

public class Car2Factory : ICarFactory
{
    private readonly IDep4 dep4;
    private readonly IDep5 dep5;
    private readonly IDep6 dep6;

    public Car1Factory(IDep4 dep4, IDep5 dep5, IDep6 dep6)
    {
        if (dep4 == null)
            throw new ArgumentNullException("dep4");
        if (dep5 == null)
            throw new ArgumentNullException("dep5");
        if (dep6 == null)
            throw new ArgumentNullException("dep6");

        this.dep4 = dep4;
        this.dep5 = dep5;
        this.dep6 = dep6;
    }

    public ICar CreateCar()
    {
        return new Car2(this.dep4, this.dep5, this.dep6);
    }

    public bool AppliesTo(Type type)
    {
        return typeof(Car2).Equals(type);
    }
}

Stratégie

public class CarStrategy : ICarStrategy
{
    private readonly ICarFactory[] carFactories;

    public CarStrategy(ICarFactory[] carFactories)
    {
        if (carFactories == null)
            throw new ArgumentNullException("carFactories");

        this.carFactories = carFactories;
    }

    public ICar CreateCar(Type type)
    {
        var carFactory = this.carFactories
            .FirstOrDefault(factory => factory.AppliesTo(type));

        if (carFactory == null)
        {
            throw new Exception("type not registered");
        }

        return carFactory.CreateCar();
    }
}

Usage

// I am showing this in code, but you would normally 
// do this with your DI container in your composition 
// root, and the instance would be created by injecting 
// it somewhere.
var strategy = new CarStrategy(new ICarFactory[] {
    new Car1Factory(dep1, dep2, dep3),
    new Car2Factory(dep4, dep5, dep6)
    });

// And then once it is injected, you would simply do this.
// Note that you could use a magic string or some other 
// data type as the parameter if you prefer.
var car1 = strategy.CreateCar(typeof(Car1));
var car2 = strategy.CreateCar(typeof(Car2));

Notez qu’en raison de l’absence d’instruction de cas de commutation, vous pouvez ajouter d’autres usines à la stratégie sans modifier la conception. Chacune de ces usines peut avoir sa propre dépendance injectée par le conteneur DI.

var strategy = new CarStrategy(new ICarFactory[] {
    new Car1Factory(dep1, dep2, dep3),
    new Car2Factory(dep4, dep5, dep6),
    new Car3Factory(dep7, dep8, dep9)
    });

var car1 = strategy.CreateCar(typeof(Car1));
var car2 = strategy.CreateCar(typeof(Car2));
var car3 = strategy.CreateCar(typeof(Car3));
59
NightOwl888

Répondez à votre commentaire sur l'exemple de code avec Composition Root. Vous pouvez créer les éléments suivants et il ne s'agit pas d'un service de localisation.

public class CarFactory
{
    private readonly Func<Type, ICar> carFactory;

    public CarFactory(Func<Type, ICar> carFactory)
    {
       this.carFactory = carFactory;
    }

    public ICar CreateCar(Type carType)
    {
        return carFactory(carType);
 }

et voici comment votre Composition Root utilise le conteneur Unity DI:

Func<Type, ICar> carFactoryFunc = type => (ICar)container.Resolve(type);
container.RegisterInstance<CarFactory>(new CarFactory(carFactoryFunc));
5
Arkadiusz K

Premièrement, vous avez une usine à béton, un conteneur IoC pourrait être une alternative plutôt que quelque chose pour vous aider.

Ensuite, il suffit de refactoriser l’usine pour ne pas s’attendre à une liste complète des paramètres possibles dans le constructeur de l’usine. C’est le principal problème. Pourquoi transmettez-vous autant de paramètres si la méthode factory n’en a pas besoin?

Je préférerais passer des paramètres spécifiques à la méthode d'usine

public abstract class CarFactoryParams { }

public class Car1FactoryParams : CarFactoryParams
{
   public Car1FactoryParams(Dep1, Dep2, Dep3) 
   { 
      this.Dep1 = Dep1;
      ...
}

public class Car2FactoryParams 
      ...

public class CarFactory
{
    public ICar CreateCar( CarFactoryParams params )
    {
        if ( params is Car1FactoryParams )
        {
            var cp = (Car1FactoryParams)params;
            return new Car1( cp.Dep1, cp.Dep2, ... );
        }
        ...
        if ( params is ...

En encapsulant la liste de paramètres dans une classe spécifique, il suffit au client de fournir exactement les paramètres requis pour l’appel de méthodes de fabrique spécifiques.

Modifier:

Malheureusement, votre message ne précise pas quels sont ces Dep1, ... et comment vous les utilisez.

Je suggère ensuite l'approche suivante qui sépare le fournisseur de l'usine de la mise en œuvre réelle de l'usine. Cette approche est connue sous le nom de Local Factory pattern:

public class CarFactory
{
   private static Func<type, ICar> _provider;

   public static void SetProvider( Func<type, ICar> provider )
   {
     _provider = provider;
   }

   public ICar CreateCar(type)
   {
     return _provider( type );
   }
}

L'usine elle-même n'a aucune implémentation, elle est ici pour définir la fondation de votre API de domaine, où vous souhaitez que vos instances de voiture soient créées avec cette API uniquement.

Ensuite, dans la racine de composition (quelque part près du point de départ de l'application où vous configurez votre conteneur actuel), vous configurez le fournisseur:

CarFactory.SetProvider(
    type =>
    {
        switch ( type )
        {
           case A:
             return _container.Resolve<ICar1>();
           case B:
             return _container.Resolve<ICar2>();
           ..
    }
);

Notez que cet exemple d'implémentation du fournisseur de la fabrique utilise un délégué, mais une interface peut également être utilisée comme spécification d'un fournisseur réel.

Cette implémentation est fondamentalement n ° 1 de votre question modifiée, cependant, elle ne présente aucun inconvénient particulier. Le client appelle toujours:

var car = new CarFactory().CreareCar( type );
1
Wiktor Zychla

J'envisagerais de donner aux dépendances une bonne structure afin que vous puissiez utiliser quelque chose de similaire à la réponse de Wiktor, mais je voudrais résumer l'automobile elle-même. Ensuite, vous n'utilisez pas la structure if..then.

public interface ICar
{
    string Make { get; set; }
    string ModelNumber { get; set; }
    IBody Body { get; set; }
    //IEngine Engine { get; set; }
    //More aspects...etc.
}

public interface IBody
{
    //IDoor DoorA { get; set; }
    //IDoor DoorB { get; set; }
    //etc
}

//Group the various specs
public interface IBodySpecs
{
    //int NumberOfDoors { get; set; }
    //int NumberOfWindows { get; set; }
    //string Color { get; set; }
}

public interface ICarSpecs
{
    IBodySpecs BodySpecs { get; set; }
    //IEngineSpecs EngineSpecs { get; set; }
    //etc.
}

public interface ICarFactory<TCar, TCarSpecs>
    where TCar : ICar
    where TCarSpecs : ICarSpecs
{
    //Async cause everything non-trivial should be IMHO!
    Task<TCar> CreateCar(TCarSpecs carSpecs);

    //Instead of having dependencies ctor-injected or method-injected
    //Now, you aren't dealing with complex overloads
    IService1 Service1 { get; set; }
    IBuilder1 Builder1 { get; set; }
}

public class BaseCar : ICar
{
    public string Make { get; set; }
    public string ModelNumber { get; set; }
    public IBody Body { get; set; }
    //public IEngine Engine { get; set; }
}

public class Van : BaseCar
{
    public string VanStyle { get; set; } 
    //etc.
}

public interface IVanSpecs : ICarSpecs
{
    string VanStyle { get; set; }
}

public class VanFactory : ICarFactory<Van, IVanSpecs>
{
    //Since you are talking of such a huge number of dependencies,
    //it may behoove you to properly categorize if they are car or 
    //car factory dependencies
    //These are injected in the factory itself
    public IBuilder1 Builder1 { get; set; }
    public IService1 Service1 { get; set; }

    public async Task<Van> CreateCar(IVanSpecs carSpecs)
    {
        var van = new Van()
        {
           //create the actual implementation here.
        };
        //await something or other
        return van;
    }
}

Je ne l'ai pas listée, mais vous pouvez maintenant mettre en œuvre plusieurs types de voitures et leurs usines correspondantes et utiliser DI pour injecter tout ce dont vous avez besoin.

1
user4275029

De nombreux conteneurs DI prennent en charge la notion de dépendances nommées.

Par exemple. (Syntaxe de Structuremap)

For<ICar>().Use<CarA>().Named("aCar");
Container.GetNamedInstance("aCar") // gives you a CarA instance

Si vous utilisez quelque chose comme une convention, une règle selon laquelle le nom est dérivé du type de voiture concret lui-même, vous vous retrouvez dans une situation où vous n'avez plus besoin de toucher l'usine lorsque vous étendez le système.

Son utilisation dans une usine est simple.

class Factory(IContainer c) {
  public ICar GetCar(string name) {
    Return c.GetNamedInstance(name);
  }
}
0
flq