c# - pattern - Patrón de fábrica pero con parámetros de objeto
factory pattern c# (7)
Tome el siguiente patrón clásico de fábrica:
public interface IPizza
{
decimal Price { get; }
}
public class HamAndMushroomPizza : IPizza
{
decimal IPizza.Price
{
get
{
return 8.5m;
}
}
}
public abstract class PizzaFactory
{
public abstract IPizza CreatePizza(ItalianPizzaFactory.PizzaType pizzaType);
}
public class ItalianPizzaFactory : PizzaFactory
{
public enum PizzaType
{
HamMushroom,
Deluxe,
Hawaiian
}
public override IPizza CreatePizza(PizzaType pizzaType)
{
switch (pizzaType)
{
case PizzaType.HamMushroom:
return new HamAndMushroomPizza();
case PizzaType.Hawaiian:
return new HawaiianPizza();
default:
throw new ArgumentException("The pizza type " + pizzaType + " is not recognized.");
}
}
}
¿Qué sucede si una (o muchas) de las pizzas de concreto requiere un parámetro específico para la implementación concreta en la construcción? Por ejemplo, digamos que la fábrica de HamAndMushroom requiere un parámetro llamado MushroomType y este parámetro sería necesario para crear una instancia del objeto.
Cuando el recuento de parámetros es muy alto, creo que la fábrica se vuelve menos útil y redundante desde el punto principal para hacer que el proceso de creación sea invisible.
Además, cuando los parámetros son ''requeridos'', también creo que Builder pierde su encanto.
En este caso, es posible que desee combinar la fábrica con un ''Objeto de parámetros'' que reduciría el número de parámetros necesarios para pasar a los métodos de fábrica estáticos y que podría haber hecho que la lógica de creación fuera más legible y ordenada que usar un Generador. Pero, por supuesto, también se necesita crear ese objeto de parámetro, pero al menos sería en una sola forma en su aplicación.
En primer lugar, me parece extraño que una clase abstracta PizzaFactory
contenga un método general abstracto CreatePizza
que tome un parámetro de un tipo más concreto ItalianPizzaFactory.PizzaType
.
Para cubrir el problema que acabo de mencionar y el problema expuesto en la publicación, sugeriría el siguiente enfoque.
public struct PizzaDefinition
{
public readonly string Tag;
public readonly string Name;
public readonly string Description;
public PizzaDefinition(string tag, string name, string description)
{
Tag = tag; Name = name; Description = description;
}
}
public abstract class PizzaFactory
{
public abstract IEnumerable<PizzaDefinition> GetMenu();
public abstract IPizza CreatePizza(PizzaDefinition pizzaDefinition);
}
public class ItalianPizzaFactory : PizzaFactory
{
public enum PizzaType
{
HamMushroom,
Deluxe,
Hawaiian
}
public override IEnumerable<PizzaDefinition> GetMenu()
{
return new PizzaDefinition[] {
new PizzaDefinition("hm:mushroom1,cheese3", "Ham&Mushroom 1", "blabla"),
new PizzaDefinition("hm:mushroom2,cheese1", "Ham&Mushroom 2", "blabla"),
new PizzaDefinition("dx", "Deluxe", "blabla"),
new PizzaDefinition("Hawaian:shrimps,caramel", "Hawaian", "blabla")
};
}
private PizzaType ParseTag(string tag, out object[] options){...}
public override IPizza CreatePizza(PizzaDefinition pizzaDefinition)
{
object[] options;
switch (ParseTag(pizzaDefinition.Tag, out options))
{
case PizzaType.HamMushroom:
return new HamAndMushroomPizza(options);
case PizzaType.Hawaiian:
return new HawaiianPizza();
default:
throw new ArgumentException("The pizza" + pizzaDefinition.Name + " is not on the menu.");
}
}
}
Como puede ver, el método ParseTag () puede ser de complejidad arbitraria, analizando un texto sin formato o un valor cifrado. O el campo Etiqueta puede ser un int simple que se asigna internamente a alguna tabla de recetas de pizza, con recetas completamente diferentes para contenido de pizza incluso ligeramente modificado.
Podría pasar un nuevo parámetro, como un mapa. Y consultar las propiedades de cada constructor concreto. Entonces todos los métodos tendrían la misma firma. Sin embargo, con esta solución, la persona que llama al constructor debe conocer las propiedades específicas del constructor concreto ... (Acoplamiento)
Puedes agregar parámetros a los métodos creadores de tu fábrica. Sin embargo, si el número de parámetros aumenta (para mí eso sería más de 2-3), y especialmente si algunos o todos esos parámetros son opcionales con valores predeterminados razonables, puede considerar convertir la fábrica en un Builder .
Eso puede ser especialmente apropiado para pizzas, donde normalmente tienes la misma corteza, solo con diferentes (combinaciones) de ingredientes. A Builder modela muy de cerca la forma común de ordenar, por ejemplo, "una pizza con salami, tomates, maíz y queso doble". OTOH para pizzas "predefinidas" es posible que desee definir métodos de fábrica de ayudantes, por ejemplo, createMargaritaPizza
o createHawaiiPizza
que luego usan el generador para crear una pizza con los ingredientes específicos de ese tipo de pizza.
Puedes probar algo como esto:
interface IPizza
{
}
class Pizza1 : IPizza
{
public Pizza1(Pizza1Parameter p)
{
}
}
class Pizza2 : IPizza
{
public Pizza2(Pizza2Parameter p)
{
}
}
interface IPizzaParameter
{
object Type { get; set; }
}
class Pizza1Parameter : IPizzaParameter
{
public object Type { get; set; }
}
class Pizza2Parameter : IPizzaParameter
{
public object Type { get; set; }
}
static class PizzaFactory
{
public enum PizzaType
{
Pizza1,
Pizza2,
}
public static IPizza CreatePizza(PizzaType type, IPizzaParameter param)
{
switch (type)
{
case PizzaType.Pizza1:
return new Pizza1(param as Pizza1Parameter);
case PizzaType.Pizza2:
return new Pizza2(param as Pizza2Parameter);
}
throw new ArgumentException();
}
}
class Program
{
static void Main()
{
var param1 = new Pizza1Parameter();
var p1 = PizzaFactory.CreatePizza(PizzaFactory.PizzaType.Pizza1, param1);
}
}
En mi humilde opinión, el concepto de fábrica con parámetros específicos de implementación parece incorrecto.
Puedes usar la reflexión:
using System.Reflection;
// ...
public override IPizza CreatePizza(PizzaType pizzaType, params object[] parameters) {
return (IPizza)
Activator.CreateInstance(
Assembly
.GetExecutingAssembly()
.GetType(pizzaType.ToString()),
parameters);
}
Tendría que agregar otro método CreatePizza () para esa clase de fábrica. Y eso significaría que los usuarios de la fábrica no podrían crear ese tipo de pizzas a menos que estuvieran usando específicamente una instancia de la clase HamAndMushroomPizzaFactory. Si simplemente tienen una referencia de PizzaFactory, solo pueden llamar a la versión sin parámetros y no podrán crear pizzas de jamón y champiñones de forma genérica.