8. Abstract Factory (Fábrica Abstracta) - Patrón Creacional

El patrón Abstract Factory ofrece una interfaz para crear familias completas de objetos relacionados o dependientes sin especificar sus clases concretas. Mientras que Factory Method delega la creación de un objeto a una subclass, Abstract Factory agrupa varios métodos factoría para garantizar que los productos creados sean compatibles entre sí.

Este patrón aparece con frecuencia en frameworks multiplataforma, motores de UI y sistemas que deben funcionar con distintos proveedores (bases de datos, servicios de mensajería, drivers de dispositivos) manteniendo coherencia entre las piezas elegidas.

8.1 Problema que resuelve

Cuando se necesitan familias de productos que deben trabajar juntas (por ejemplo, botones y casillas de verificación con el mismo estilo), instanciar clases concretas en el código cliente introduce dependencias fuertes y riesgo de mezclas incompatibles. Además, cada lugar donde se crea un objeto concreto repite lógica de inicialización y dificulta sustituir proveedores o estilos en tiempo de ejecución.

Abstract Factory encapsula los detalles de construcción de cada familia. El cliente solicita a la fábrica los productos que necesita y recibe implementaciones consistentes sin conocer las clases concretas. Cambiar de familia se reduce a usar otra fábrica, incluso en tiempo de ejecución.

8.2 Intención y motivación

La intención principal es proporcionar un objeto que encapsule un conjunto de métodos factoría, cada uno responsable de crear un miembro de la familia. De esta manera, la fábrica asegura la combinación adecuada de componentes.

En interfaces gráficas, por ejemplo, se evita mezclar widgets al estilo Windows con los de macOS en la misma pantalla. En la capa de persistencia, permite elegir entre distintos motores (MySQL, PostgreSQL, MongoDB) manteniendo coordinados los repositorios y conexiones.

8.3 Estructura y participantes

Los participantes fundamentales son:

  • Fábrica abstracta: declara los métodos factoría para cada producto de la familia.
  • Fábricas concretas: implementan los métodos para producir variantes específicas de la familia.
  • Producto abstracto: interfaz o clase base que define las operaciones de cada tipo de producto.
  • Productos concretos: implementaciones específicas de cada producto abstracto.
  • Cliente: utiliza la fábrica abstracta para crear productos y opera únicamente con sus interfaces.

La fábrica abstracta suele entregarse a través de inyección de dependencias o patrones de configuración. El cliente no conoce cómo se construyen los objetos ni sus clases concretas, solo espera que cumplan la interfaz.

8.4 Diferencias con Factory Method

Factory Method se enfoca en crear un solo producto y deja la elección a subclasses del creador. Abstract Factory, en cambio, agrupa varios métodos factoría, uno por cada producto de la familia. De hecho, las fábricas concretas suelen implementar sus métodos usando Factory Method internamente.

Si solo se requiere un tipo de producto, Abstract Factory puede resultar excesivo. Su fortaleza aparece cuando hay múltiples productos que deben coordinarse (por ejemplo, DAO, servicios de transacción y migradores de esquemas de una misma base de datos).

8.5 Implementación básica en C#

El siguiente ejemplo en C# muestra una fábrica para componentes de interfaz: botones y casillas. Cada fábrica concreta produce ambos productos con una apariencia coherente.

using System;

public interface IBoton {
    void Dibujar();
}

public interface ICasilla {
    void Marcar();
}

public class BotonWindows : IBoton {
    public void Dibujar() {
        Console.WriteLine("Boton estilo Windows");
    }
}

public class BotonMac : IBoton {
    public void Dibujar() {
        Console.WriteLine("Boton estilo macOS");
    }
}

public class CasillaWindows : ICasilla {
    public void Marcar() {
        Console.WriteLine("Casilla estilo Windows");
    }
}

public class CasillaMac : ICasilla {
    public void Marcar() {
        Console.WriteLine("Casilla estilo macOS");
    }
}

public interface IFabricaComponentes {
    IBoton CrearBoton();
    ICasilla CrearCasilla();
}

public class FabricaWindows : IFabricaComponentes {
    public IBoton CrearBoton() {
        return new BotonWindows();
    }

    public ICasilla CrearCasilla() {
        return new CasillaWindows();
    }
}

public class FabricaMac : IFabricaComponentes {
    public IBoton CrearBoton() {
        return new BotonMac();
    }

    public ICasilla CrearCasilla() {
        return new CasillaMac();
    }
}

public class UiBuilder {
    private readonly IFabricaComponentes _fabrica;

    public UiBuilder(IFabricaComponentes fabrica) {
        _fabrica = fabrica;
    }

    public void Renderizar() {
        var boton = _fabrica.CrearBoton();
        var casilla = _fabrica.CrearCasilla();
        boton.Dibujar();
        casilla.Marcar();
    }
}

El cliente recibe una instancia de IFabricaComponentes y crea los componentes sin conocer sus clases concretas, garantizando compatibilidad.

8.6 Ejemplo completo en C#: persistencia multiplataforma

Supongamos un sistema que debe trabajar indistintamente con una base relacional o con una base de documentos. Cada proveedor requiere repositorios, transacciones y migradores diferentes. Implementamos Abstract Factory para encapsular las familias.

using System;

public interface IConexion {
    void Abrir();
    void Cerrar();
}

public interface IRepositorioCliente {
    void Guardar(string nombre);
}

public interface IMigradorEsquema {
    void Ejecutar();
}

public class ConexionMySql : IConexion {
    public void Abrir() {
        Console.WriteLine("Abriendo conexion MySQL");
    }

    public void Cerrar() {
        Console.WriteLine("Cerrando conexion MySQL");
    }
}

public class ConexionMongo : IConexion {
    public void Abrir() {
        Console.WriteLine("Abriendo conexion MongoDB");
    }

    public void Cerrar() {
        Console.WriteLine("Cerrando conexion MongoDB");
    }
}

public class RepositorioClienteMySql : IRepositorioCliente {
    public void Guardar(string nombre) {
        Console.WriteLine($"Guardando {nombre} en tabla Clientes (MySQL)");
    }
}

public class RepositorioClienteMongo : IRepositorioCliente {
    public void Guardar(string nombre) {
        Console.WriteLine($"Guardando {nombre} en coleccion Customers (MongoDB)");
    }
}

public class MigradorMySql : IMigradorEsquema {
    public void Ejecutar() {
        Console.WriteLine("Ejecutando migraciones para MySQL");
    }
}

public class MigradorMongo : IMigradorEsquema {
    public void Ejecutar() {
        Console.WriteLine("Ejecutando migraciones para MongoDB");
    }
}

public interface IFabricaPersistencia {
    IConexion CrearConexion();
    IRepositorioCliente CrearRepositorioCliente();
    IMigradorEsquema CrearMigrador();
}

public class FabricaMySql : IFabricaPersistencia {
    public IConexion CrearConexion() {
        return new ConexionMySql();
    }

    public IRepositorioCliente CrearRepositorioCliente() {
        return new RepositorioClienteMySql();
    }

    public IMigradorEsquema CrearMigrador() {
        return new MigradorMySql();
    }
}

public class FabricaMongo : IFabricaPersistencia {
    public IConexion CrearConexion() {
        return new ConexionMongo();
    }

    public IRepositorioCliente CrearRepositorioCliente() {
        return new RepositorioClienteMongo();
    }

    public IMigradorEsquema CrearMigrador() {
        return new MigradorMongo();
    }
}

public class ServicioClientes {
    private readonly IFabricaPersistencia _fabrica;

    public ServicioClientes(IFabricaPersistencia fabrica) {
        _fabrica = fabrica;
    }

    public void RegistrarCliente(string nombre) {
        var conexion = _fabrica.CrearConexion();
        var repositorio = _fabrica.CrearRepositorioCliente();
        var migrador = _fabrica.CrearMigrador();

        migrador.Ejecutar();
        conexion.Abrir();
        repositorio.Guardar(nombre);
        conexion.Cerrar();
    }
}

public static class DemoPersistencia {
    public static void Main() {
        IFabricaPersistencia fabrica = new FabricaMySql();
        var servicio = new ServicioClientes(fabrica);
        servicio.RegistrarCliente("Alice");

        fabrica = new FabricaMongo();
        servicio = new ServicioClientes(fabrica);
        servicio.RegistrarCliente("Bob");
    }
}

La clase ServicioClientes no conoce los detalles de cada proveedor. Cambiar de motor es tan simple como entregar otra fábrica concreta, lo que habilita configuraciones por entorno o pruebas aisladas.

Abstract Factory

8.7 Ventajas y beneficios

  • Mantiene consistencia entre los productos de una familia y evita combinaciones incompatibles.
  • Facilita cambiar de proveedor o estilo en tiempo de ejecución al encapsular la creación en una sola fábrica.
  • Sigue el principio de abierto/cerrado: agregar una nueva familia implica crear una fábrica concreta y sus productos sin modificar el código existente.
  • Permite centralizar configuraciones complejas (dependencias, cachés, colas) en un solo lugar.

8.8 Desventajas y riesgos

El patrón introduce varias interfaces y clases, lo que puede resultar excesivo si el dominio es pequeño o si los productos no requieren evolucionar en bloques. Además, si aparecen nuevos tipos de productos dentro de la familia, es necesario modificar todas las fábricas existentes, lo que puede romper el principio de abierto/cerrado desde la perspectiva de las fábricas.

Para mitigar el crecimiento excesivo de clases, conviene agrupar la declaración de interfaces en paquetes claros y apoyarse en patrones complementarios como Prototype o Builder cuando los productos necesitan inicializaciones complejas.

8.9 Cuándo elegir Abstract Factory

Es idóneo cuando un sistema debe:

  • Soportar múltiples plataformas o estilos visuales con componentes coherentes.
  • Integrarse con diferentes proveedores de servicios manteniendo un API uniforme.
  • Seleccionar una familia completa de objetos en tiempo de configuración o ejecución.
  • Exponer puntos de extensión en un framework que permitan a terceros crear implementaciones compatibles.

Si solo se necesita intercambiar un único objeto, considere Factory Method o Strategy. Si se debe clonar o copiar objetos preexistentes, Prototype puede ser más adecuado.