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 Java

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

public interface Boton {
    void dibujar();
}

public interface Checkbox {
    void dibujar();
}

public class BotonWindows implements Boton {
    @Override
    public void dibujar() {
        System.out.println("Botón estilo Windows");
    }
}

public class CheckboxWindows implements Checkbox {
    @Override
    public void dibujar() {
        System.out.println("Checkbox estilo Windows");
    }
}

public class BotonMac implements Boton {
    @Override
    public void dibujar() {
        System.out.println("Botón estilo macOS");
    }
}

public class CheckboxMac implements Checkbox {
    @Override
    public void dibujar() {
        System.out.println("Checkbox estilo macOS");
    }
}

public interface FabricaUI {
    Boton crearBoton();
    Checkbox crearCheckbox();
}

public class FabricaWindows implements FabricaUI {
    @Override
    public Boton crearBoton() {
        return new BotonWindows();
    }

    @Override
    public Checkbox crearCheckbox() {
        return new CheckboxWindows();
    }
}

public class FabricaMac implements FabricaUI {
    @Override
    public Boton crearBoton() {
        return new BotonMac();
    }

    @Override
    public Checkbox crearCheckbox() {
        return new CheckboxMac();
    }
}

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

8.6 Ejemplo completo en Java: 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.

public interface Conexion {
    void abrir();
    void cerrar();
}

public interface RepositorioCliente {
    void guardar(String nombre);
}

public interface MigradorEsquema {
    void ejecutar();
}

public class ConexionMySql implements Conexion {
    @Override
    public void abrir() {
        System.out.println("Conexión MySQL abierta");
    }

    @Override
    public void cerrar() {
        System.out.println("Conexión MySQL cerrada");
    }
}

public class RepositorioClienteMySql implements RepositorioCliente {
    @Override
    public void guardar(String nombre) {
        System.out.println("Insertando cliente en tabla MySQL: " + nombre);
    }
}

public class MigradorMySql implements MigradorEsquema {
    @Override
    public void ejecutar() {
        System.out.println("Ejecutando scripts SQL de migración");
    }
}

public class ConexionMongo implements Conexion {
    @Override
    public void abrir() {
        System.out.println("Conexión MongoDB abierta");
    }

    @Override
    public void cerrar() {
        System.out.println("Conexión MongoDB cerrada");
    }
}

public class RepositorioClienteMongo implements RepositorioCliente {
    @Override
    public void guardar(String nombre) {
        System.out.println("Insertando documento en MongoDB: " + nombre);
    }
}

public class MigradorMongo implements MigradorEsquema {
    @Override
    public void ejecutar() {
        System.out.println("Creando índices y colecciones en MongoDB");
    }
}

public interface FabricaPersistencia {
    Conexion crearConexion();
    RepositorioCliente crearRepositorioCliente();
    MigradorEsquema crearMigrador();
}

public class FabricaMySql implements FabricaPersistencia {
    @Override
    public Conexion crearConexion() {
        return new ConexionMySql();
    }

    @Override
    public RepositorioCliente crearRepositorioCliente() {
        return new RepositorioClienteMySql();
    }

    @Override
    public MigradorEsquema crearMigrador() {
        return new MigradorMySql();
    }
}

public class FabricaMongo implements FabricaPersistencia {
    @Override
    public Conexion crearConexion() {
        return new ConexionMongo();
    }

    @Override
    public RepositorioCliente crearRepositorioCliente() {
        return new RepositorioClienteMongo();
    }

    @Override
    public MigradorEsquema crearMigrador() {
        return new MigradorMongo();
    }
}

public class ServicioClientes {
    private final FabricaPersistencia fabrica;

    public ServicioClientes(FabricaPersistencia fabrica) {
        this.fabrica = fabrica;
    }

    public void registrarCliente(String nombre) {
        Conexion conexion = fabrica.crearConexion();
        RepositorioCliente repositorio = fabrica.crearRepositorioCliente();
        MigradorEsquema migrador = fabrica.crearMigrador();

        migrador.ejecutar();
        conexion.abrir();
        repositorio.guardar(nombre);
        conexion.cerrar();
    }
}

public class Aplicacion {
    public static void main(String[] args) {
        FabricaPersistencia fabrica = new FabricaMySql();
        ServicioClientes 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.