Implementación del Patrón Abstract Factory en C# y el Principio de Inversión de Dependencias

Cuando una clase instancia directamente sus dependencias mediante el operador new, se genera un acoplamiento fuerte hacia implementaciones concretas. Este enfoque dificulta la mantenibilidad, la escalabilidad y las pruebas unitarias. Para mitigar este problema, el Principio de Inversión de Dependencias (DIP) establece que los módulos de alto nivel no deben depender de módulos de bajo nivel; ambos deben depender de abstracciones. Asimismo, las abstracciones no deben depender de los detalles, sino al revés.

Aplicar este principio en la práctica implica seguir tres directrices fundamentales:

  • Evitar que las variables mantengan referencias a clases concretas. Utilizar fábricas o inyección de dependencias rompe este vínculo directo.
  • No heredar de clases concretas. La herencia debe partir siempre de interfaces o clases base abstractas para preservar la flexibilidad.
  • No sobrescribir métodos que ya contienen lógica implementada en la clase base. Dichos métodos representan comportamiento compartido que no debería alterarse en las subclases.

El patrón Factory Method es una herramienta eficaz para cumplir con el DIP, ya que delega la creación de objetos a subclases. Sin embargo, cuando un sistema requiere crear familias de objetos relacionados o que deben usarse conjuntamente, el patrón Abstract Factory resulta más adecuado. Este patrón proporciona una interfaz para crear conjuntos de objetos dependientes sin especificar sus clases concretas, y frecuentemente se combina con Factory Method para evitar la proliferación descontrolada de clases (conocida como "explosión de tipos").

A continuación, se muestra una implementación en C# que ilustra esta sinergia. Se parte de una clase base que orquesta el flujo de trabajo y define el método de fábrica:

public abstract class GestorDePedidos
{
    public ProductoBase ProcesarPedido(string categoria)
    {
        var producto = FabricarProducto(categoria);
        producto.Preparar();
        producto.Cocinar();
        producto.Empaquetar();
        return producto;
    }

    protected abstract ProductoBase FabricarProducto(string categoria);
}

La clase concreta que hereda de GestorDePedidos utiliza una fábrica de ingredientes para construir los productos. Note cómo se delega la responsabilidad de creación y se emplea una expresión switch moderna para mayor legibilidad:

public class SucursalMetropolitana : GestorDePedidos
{
    protected override ProductoBase FabricarProducto(string categoria)
    {
        var proveedor = new FabricaIngredientesLocal();
        
        return categoria switch
        {
            "Queso" => new ProductoQueso(proveedor),
            "Mariscos" => new ProductoMariscos(proveedor),
            _ => throw new ArgumentException("Categoría no soportada")
        };
    }
}

Aunque SucursalMetropolitana conoce las clases concretas de los productos, esta sección del código suele ser estable. La verdadera flexibilidad reside en ProductoBase, que opera exclusivamente contra abstracciones:

public abstract class ProductoBase
{
    public string Nombre { get; set; }
    public Masa TipoMasa { get; set; }
    public Salsa TipoSalsa { get; set; }
    public Queso TipoQueso { get; set; }
    public Vegetales[] Verduras { get; set; }

    public abstract void Preparar();

    public void Cocinar() => Console.WriteLine("Horneando producto...");
    public void Empaquetar() => Console.WriteLine("Empaquetando para entrega...");
    
    public override string ToString() => Nombre;
}

La implementación concreta del producto recibe la fábrica de ingredientes mediante inyección por constructor. Esto favorece la composición sobre la herencia, permitiendo cambiar el comportamiento en tiempo de ejecución sin modificar la jerarquía de clases:

public class ProductoQueso : ProductoBase
{
    private readonly IFabricaDeComponentes _componentes;

    public ProductoQueso(IFabricaDeComponentes componentes)
    {
        _componentes = componentes;
    }

    public override void Preparar()
    {
        Console.WriteLine($"Preparando {Nombre}");
        TipoMasa = _componentes.ObtenerMasa();
        TipoSalsa = _componentes.ObtenerSalsa();
        TipoQueso = _componentes.ObtenerQueso();
    }
}

La interfaz IFabricaDeComponentes define el contrato para generar la familia de ingredientes. Cada región o variante del producto puede implementar esta interfaz con sus propias clases concretas, garantizando que los componentes sean compatibles entre sí:

public interface IFabricaDeComponentes
{
    Masa ObtenerMasa();
    Salsa ObtenerSalsa();
    Queso ObtenerQueso();
    Vegetales[] ObtenerVegetales();
}

public class FabricaIngredientesLocal : IFabricaDeComponentes
{
    public Masa ObtenerMasa() => new MasaFina();
    public Salsa ObtenerSalsa() => new SalsaTomateBasica();
    public Queso ObtenerQueso() => new QuesoMozzarella();
    
    public Vegetales[] ObtenerVegetales() => new Vegetales[] 
    { 
        new Cebolla(), new Pimiento(), new Champinon() 
    };
}

Conclusiones técnicas:

  • El patrón Abstract Factory no suele utilizarse de forma aislada. Se integra naturalmente dentro de una estructura basada en Factory Method para gestionar familias de objetos relacionados.
  • Mientras Factory Method se apoya en la herencia y el polimorfismo para delegar la creación a subclases, Abstract Factory prioriza la composición y expone métodos de creación a través de una interfaz común.
  • La combinación de ambos patrones previene la "explosión de tipos", ya que centraliza la lógica de instacniación de componentes dependientes en un único punto extensible.
  • Aplicar el Principio de Inversión de Dependencias junto con estos patrones reduce drásticamente el acoplamiento, facilitando la sustitución de implementaciones y mejorando la testabilidad del sistema.
  • Las fábricas, en cualquiera de sus variantes, son mecanismos de encapsulación cuyo objetivo principle es aislar la lógica de creación del resto de la aplicación, promoviendo el diseño orientado a contratos en lugar de implementaciones concretas.

Etiquetas: C# Abstract Factory Dependency Inversion Principle Factory Method design patterns

Publicado el 9-27 07:01