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.
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.
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.
Los participantes fundamentales son:
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.
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).
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.
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.
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.
Es idóneo cuando un sistema debe:
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.