Método de fábrica con DI e IoC

Resuelto MistyK asked hace 9 años • 6 respuestas

Estoy familiarizado con estos patrones pero todavía no sé cómo manejar la siguiente situación:

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 general, el problema es la cantidad de referencias que es necesario inyectar. Será aún peor cuando haya más coches.

El primer enfoque que me viene a la mente es inyectar Car1 y Car2 en el constructor de fábrica, pero va en contra del enfoque de fábrica porque la fábrica devolverá siempre el mismo objeto. El segundo enfoque es inyectar servicelocator pero es antipatrón en todas partes. ¿Cómo resolverlo?

Editar:

Forma alternativa 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;
            }
     }
}

Forma alternativa 2 (demasiado difícil de usar debido a demasiadas dependencias en el árbol):

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;
            }
     }
}
MistyK avatar Aug 12 '15 02:08 MistyK
Aceptado

Tener una declaración de caso de cambio dentro de una fábrica es un olor a código. Curiosamente, no pareces estar concentrado en resolver ese problema en absoluto.

La mejor y más amigable solución DI para este escenario es el patrón de estrategia . Permite que su contenedor DI inyecte las dependencias en las instancias de fábrica a las que pertenecen, sin saturar otras clases con esas dependencias ni recurrir a un localizador de servicios.

Interfaces

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

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

Fábricas

public class Car1Factory : ICarFactory
{
    private readonly IDep1 dep1;
    private readonly IDep2 dep2;
    private readonly IDep3 dep3;
    
    public Car1Factory(IDep1 dep1, IDep2 dep2, IDep3 dep3)
    {
        this.dep1 = dep1 ?? throw new ArgumentNullException(nameof(dep1));
        this.dep2 = dep2 ?? throw new ArgumentNullException(nameof(dep2));
        this.dep3 = dep3 ?? throw new ArgumentNullException(nameof(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 Car2Factory(IDep4 dep4, IDep5 dep5, IDep6 dep6)
    {
        this.dep4 = dep4 ?? throw new ArgumentNullException(nameof(dep4));
        this.dep5 = dep5 ?? throw new ArgumentNullException(nameof(dep5));
        this.dep6 = dep6 ?? throw new ArgumentNullException(nameof(dep6));
    }
    
    public ICar CreateCar()
    {
        return new Car2(this.dep4, this.dep5, this.dep6);
    }
    
    public bool AppliesTo(Type type)
    {
        return typeof(Car2).Equals(type);
    }
}

Estrategia

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

    public CarStrategy(ICarFactory[] carFactories)
    {
        this.carFactories = carFactories ?? throw new ArgumentNullException(nameof(carFactories));
    }
    
    public ICar CreateCar(Type type)
    {
        var carFactory = this.carFactories
            .FirstOrDefault(factory => factory.AppliesTo(type));
            
        if (carFactory == null)
        {
            throw new InvalidOperationException($"{type} not registered");
        }
        
        return carFactory.CreateCar();
    }
}

Uso

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

Tenga en cuenta que debido a que no existe una declaración de cambio de caso, puede agregar fábricas adicionales a la estrategia sin cambiar el diseño, y cada una de esas fábricas puede tener sus propias dependencias que son inyectadas por el contenedor 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));
NightOwl888 avatar Aug 12 '2015 17:08 NightOwl888

Respondí una pregunta similar hace algún tiempo. Básicamente se trata de tu elección. Tienes que elegir entre detalle (que te brinda más ayuda de un compilador) y automatización, que te permite escribir menos código pero es más propenso a errores.

Esta es mi respuesta que respalda la verbosidad.

Y esta también es una buena respuesta que respalda la automatización.

EDITAR

Creo que el enfoque que usted considera incorrecto es en realidad el mejor. A decir verdad, normalmente no habrá tantas dependencias allí. Me gusta este enfoque porque es muy explícito y rara vez genera errores de ejecución.

Forma alternativa 1:

Éste es malo. En realidad, es un localizador de servicios, que se considera un antipatrón .

Manera alternativa 2

Como escribiste, no es fácil de usar si se mezcla con un contenedor del COI. Sin embargo, en algunos casos, un enfoque similar ( ID del pobre ) puede resultar útil.

Considerándolo todo, no me molestaría en tener "muchas" dependencias en sus fábricas. Es un código simple y declarativo. Se tarda unos segundos en escribir y puede ahorrarle horas de lucha con errores de tiempo de ejecución.

Andrzej Gis avatar Aug 11 '2015 20:08 Andrzej Gis

Consideraría darle a las dependencias una buena estructura para que puedas utilizar algo similar a la respuesta de Wiktor, pero abstraería la propia fábrica de automóviles. Entonces, no usas la estructura si...entonces.

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

No lo mencioné, pero ahora puedes implementar múltiples tipos de automóviles y sus fábricas correspondientes y usar DI para inyectar lo que necesites.

 avatar Aug 11 '2015 20:08

En primer lugar, tiene una fábrica de hormigón, un contenedor de IoC podría ser una alternativa en lugar de algo que le ayude allí.

Luego, simplemente refactorice la fábrica para no esperar una lista completa de parámetros posibles en el constructor de la fábrica. Este es el problema principal: ¿por qué se pasan tantos parámetros si el método de fábrica no los necesita?

Prefiero pasar parámetros específicos al método de fábrica.

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 ...

Al encapsular la lista de parámetros en una clase específica, simplemente hace que el cliente proporcione exactamente estos parámetros necesarios para la invocación de un método de fábrica específico.

Editar:

Desafortunadamente, en tu publicación no quedó claro qué son estos Dep1y cómo se usan.

Sugiero seguir un enfoque que separe al proveedor de fábrica de la implementación real de la fábrica. Este enfoque se conoce como patrón de fábrica local :

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

La fábrica en sí no tiene ninguna implementación, está aquí para sentar las bases de la API de su dominio, donde desea que las instancias de su automóvil se creen solo con esta API.

Luego, en la raíz de composición (en algún lugar cerca del punto de inicio de la aplicación donde configura su contenedor real), configura el proveedor:

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

Tenga en cuenta que esta implementación de ejemplo del proveedor de la fábrica utiliza un delegado, pero también se podría utilizar una interfaz como especificación para un proveedor real.

Esta implementación es básicamente la número 1 de su pregunta editada; sin embargo, no tiene desventajas particulares. El cliente todavía llama:

var car = new CarFactory().CreareCar( type );
Wiktor Zychla avatar Aug 11 '2015 19:08 Wiktor Zychla