Abstract Factory Pattern

Intención y problema

Abstract Factory ofrece un contrato para crear familias de productos relacionados sin acoplar al consumidor con sus clases concretas. La formulación original GoF pone el foco en productos diseñados para usarse conjuntamente. Extracto original: Abstract Factory.

El problema aparece cuando elegir cada implementación por separado permite combinaciones que contradicen una restricción del diseño. La decisión importante es qué familia utilizar, no simplemente dónde escribir llamadas de construcción.

Una factory que crea varios objetos sin relación puede ser un registro de constructores conveniente, pero la cantidad no demuestra Abstract Factory. Debe existir una razón para seleccionar los productos como conjunto: protocolo, entorno, capacidades o alguna otra compatibilidad significativa.

Fuerzas, participantes y estructura

Client → AbstractFactory
           ├── createProductA() → ProductA
           └── createProductB() → ProductB
 
ConcreteFactory1 → ProductA1 + ProductB1
ConcreteFactory2 → ProductA2 + ProductB2

AbstractFactory define operaciones para obtener tipos de producto de la familia. ConcreteFactory selecciona sus implementaciones compatibles. Cada Product expresa el contrato que usa el cliente; ConcreteProduct implementa la variante correspondiente.

El cliente conoce las operaciones de la factory y los contratos de producto. La selección concreta ocurre en una frontera de configuración. Esto reduce conocimiento de implementaciones en el consumidor, pero mantiene una decisión de familia que alguien debe tomar y validar.

La tensión consiste en preservar consistencia conjunta sin volver rígido cada detalle. Una interfaz demasiado amplia termina vinculando productos que evolucionan independientemente. Una interfaz demasiado vaga deja al cliente deduciendo qué obtuvo y si puede combinarlo.

Colaboración: una familia de exportación

En un ejemplo propio, ExportKitFactory crea un Formatter y un Validator para una especificación de intercambio. V1ExportKitFactory produce FormatterV1 y ValidatorV1; V2ExportKitFactory entrega sus equivalentes V2. El flujo selecciona una familia para la versión requerida y obtiene ambos productos de ella.

Select export specification V2
    → V2ExportKitFactory
        → FormatterV2
        → ValidatorV2
    → Format and validate according to V2

La restricción de este ejemplo exige que formato y validación correspondan a la misma especificación. Seleccionar FormatterV2 y ValidatorV1 por decisiones independientes podría rechazar resultados válidos o aceptar datos incompatibles.

Eso no significa que todas las familias posibles sean incompatibles entre sí. La validez de mezclar productos depende del contrato real. Si un validador admite ambas versiones y esa garantía está documentada, la mezcla puede ser legítima; la factory debe reflejar esa política, no inventar prohibiciones universales.

Elegir una familia una vez reduce decisiones dispersas. Si la selección ocurre por operación o por cliente, el alcance debe ser explícito. Cambiar una configuración no transforma automáticamente productos ya creados ni migra su estado.

Compatibilidad como propiedad del conjunto

DimensiónQué podría requerir una familia
VisualConvenciones coherentes de presentación y comportamiento.
ProtocolProductos que hablan la misma versión y representación.
RuntimeRecursos compatibles con el mismo entorno de ejecución.
Vendor-specificCapacidades válidas para un proveedor determinado.
Environment-specificProductos coherentes con el entorno seleccionado.
Feature-specificVariantes que cumplen un conjunto acordado de capacidades.

No hace falta exigir todas esas dimensiones simultáneamente. Identificar la relevante evita que family sea una etiqueta vacía. En ExportKit, el criterio principal es el contrato de intercambio; que dos objetos compartan proveedor podría no bastar.

Compartir tipos abstractos tampoco demuestra compatibilidad semántica. Un Formatter puede devolver bytes y un Validator aceptar bytes, mientras difieren en representación o versión. Las pruebas deben cubrir esa cooperación, además de que cada producto satisfaga su interfaz por separado.

Una factory ayuda a controlar selección, pero un consumidor que construye productos por fuera puede saltarse la restricción. Según el contexto, el diseño puede necesitar límites adicionales, identificación de familia o validaciones explícitas. El nombre del patrón no impone por sí solo esas garantías.

Consecuencias y costo de evolución

El consumidor puede trabajar con contratos estables y cambiar la familia en el punto de configuración. También se vuelve más visible una restricción conjunta que antes dependía de convenciones dispersas.

El costo es mantener operaciones y contratos para varias clases de producto. Si cada producto requiere parámetros diferentes, la interfaz puede volverse difícil de usar o demasiado general. Las dependencias y el ownership de recursos también deben conservarse claros al delegar construcción.

Agregar una familia y agregar un tipo de producto son cambios distintos. Una nueva familia puede implementar las operaciones existentes si el contrato permanece suficiente. Agregar otro tipo suele exigir ampliar la interfaz y revisar las factories que deben proporcionarlo; esa tensión figura en el análisis original GoF.

En ExportKit, una familia V3 podría aportar FormatterV3 y ValidatorV3 sin cambiar qué pide el cliente. Incorporar createSigner, en cambio, obliga a decidir qué signer corresponde a cada familia y cómo evolucionan los consumidores. No existe extensión ilimitada sin costos.

Reemplazar métodos explícitos por un Create(name) genérico no elimina la tensión: puede trasladarla a claves, validación y resultados menos precisos. Evaluar el nuevo tipo de producto requiere decidir si realmente pertenece a la misma familia o debería tener otra responsabilidad.

Abstract Factory frente a Factory Method

factory-method-pattern organiza variación en una operación de creación mediante Creator y sus extensiones. Abstract Factory organiza un contrato de creación para una familia. Una pregunta mira el punto de creación; la otra mira la coherencia de varios productos.

Una Abstract Factory puede implementar internamente sus operaciones mediante Factory Methods, como explica GoF. Esa combinación no obliga al cliente a conocer subclases ni convierte ambos nombres en sinónimos.

Si solo varía un Exporter y no existe relación relevante con otros productos, puede bastar una función de creación o un punto de extensión. Introducir una familia ficticia para justificar varias factories agrega vocabulario sin expresar una restricción real.

Abstract Factory frente a Builder y DI

builder-pattern construye progresivamente un producto complejo y separa proceso de representación. Abstract Factory proporciona productos relacionados según una selección de familia. Las preguntas son distintas: ¿necesito etapas de construcción o consistencia entre los productos que obtengo?

Pueden combinarse. Una factory puede proporcionar un builder adecuado a la familia, o un builder recibir productos compatibles para ensamblar su resultado. En ambos casos hay que explicar qué responsabilidad preserva la familia y cuál administra el proceso.

Un DI container resuelve y suministra dependencias mediante infraestructura general; Fowler distingue configuración de uso y cómo se proporcionan los colaboradores. Abstract Factory hace explícito un contrato de creación con significado de diseño. Suministrar una factory mediante DI es posible, pero el container no se convierte automáticamente en ese contrato ni garantiza compatibilidad conjunta.

En ExportKit, registrar Formatter y Validator independientemente puede permitir versiones diferentes. Un registro coordinado podría mantener la restricción; habría que comprobarlo. Usar un container no sustituye la decisión de familia ni las pruebas que la respaldan.

Alternativas modernas, aplicabilidad y mal uso

Un módulo, record de funciones o bundle de closures puede expresar operaciones de creación relacionadas sin una jerarquía extensa. Lo importante es conservar la selección conjunta y sus garantías. La retrospectiva de autores GoF reconoce que características del lenguaje pueden cambiar cómo se expresa una solución.

Tampoco hay una obligación de hacer global la factory o convertirla en Singleton. Su alcance debe corresponder a la familia, configuración y lifecycle que necesita el consumidor.

Abstract Factory es candidata cuando varias familias reales comparten tipos de producto y la compatibilidad conjunta importa. creational-patterns y pattern-selection ayudan a compararla con alternativas más pequeñas. Para una única variante sencilla, construcción directa puede comunicar mejor el diseño.

Los malos usos incluyen crear una familia cuando solo varía un producto, retornar objetos sin relación, multiplicar interfaces para evitar new y reproducir toda la jerarquía concreta en factories sin reducir dependencias. También ampliar indefinidamente una interfaz mientras cada nuevo tipo exige modificaciones generales.

La revisión debe poder explicar qué selección queda centralizada, qué combinaciones acepta el contrato y cuánto cuesta agregar una familia o un producto. Si esa explicación falta, la organización probablemente está ocultando construcción, sin representar todavía una familia con significado.