Abstract Factory para Familias de Componentes UI
Crea familias de objetos relacionados sin especificar clases concretas, permitiendo implementaciones específicas de plataforma que comparten una interfaz común.
Resumen
Despacha un botón a tres plataformas y el problema aparece rápido: Web quiere un <button>, iOS quiere un UIButton, Android quiere un widget Material. Todos son “un botón” pero no son intercambiables — mezcla un checkbox web en un formulario nativo y la pantalla se rompe. El patrón Abstract Factory crea familias enteras de objetos relacionados a través de una interfaz, así que cada producto que recibes pertenece a la misma plataforma o tema.
Esta página cubre la aplicación de Abstract Factory a familias de componentes UI cross-platform. Para los fundamentos del patrón y otras aplicaciones, mira la página genérica de Abstract Factory.
Cuándo Usar Esto
- Un sistema debe soportar múltiples plataformas o temas con familias de componentes cohesionadas
- Necesitas garantizar que productos de la misma factory sean compatibles — un diálogo nativo no puede convivir con un checkbox web
- El proceso de creación debería estar oculto del código cliente que no debería saber en qué plataforma está
Evítalo cuando:
- La familia es chica y estable — dos o tres productos no justifican las interfaces extra; un Factory Method alcanza
- Las diferencias entre plataformas son cosméticas (colores, espaciado) — variables CSS o un theme object son más simples que implementaciones separadas
- Solo existe una plataforma hoy; agrega la abstracción cuando la segunda familia realmente llegue
Problema
Un toolkit de UI necesita renderizar botones nativos, checkboxes y dialogs en Web, iOS y Android. Usar Factory Method para cada widget por separado corre el riesgo de mezclar componentes nativos y web en la misma vista.
Solución
// factory/UIFactory.ts
interface Button {
render(): string;
onClick(handler: () => void): void;
}
interface Checkbox {
render(): string;
toggle(): void;
isChecked(): boolean;
}
interface Dialog {
show(): void;
hide(): void;
setTitle(title: string): void;
}
interface UIFactory {
createButton(label: string): Button;
createCheckbox(label: string): Checkbox;
createDialog(): Dialog;
}
// Familia Web
class WebButton implements Button {
constructor(private label: string) {}
render(): string { return `<button>${this.label}</button>`; }
onClick(handler: () => void): void { /* attach DOM listener */ }
}
class WebCheckbox implements Checkbox {
private checked = false;
constructor(private label: string) {}
render(): string { return `<label><input type="checkbox"> ${this.label}</label>`; }
toggle(): void { this.checked = !this.checked; }
isChecked(): boolean { return this.checked; }
}
class WebDialog implements Dialog {
private title = '';
show(): void { console.log('Show web dialog:', this.title); }
hide(): void { console.log('Hide web dialog'); }
setTitle(title: string): void { this.title = title; }
}
class WebFactory implements UIFactory {
createButton(label: string): Button { return new WebButton(label); }
createCheckbox(label: string): Checkbox { return new WebCheckbox(label); }
createDialog(): Dialog { return new WebDialog(); }
}
// Familia Mobile
class MobileButton implements Button {
constructor(private label: string) {}
render(): string { return `[Mobile Button: ${this.label}]`; }
onClick(): void { /* native tap handler */ }
}
class MobileCheckbox implements Checkbox {
private checked = false;
constructor(private label: string) {}
render(): string { return `[Mobile Switch: ${this.label}]`; }
toggle(): void { this.checked = !this.checked; }
isChecked(): boolean { return this.checked; }
}
class MobileDialog implements Dialog {
private title = '';
show(): void { console.log('Show mobile modal:', this.title); }
hide(): void { console.log('Dismiss mobile modal'); }
setTitle(title: string): void { this.title = title; }
}
class MobileFactory implements UIFactory {
createButton(label: string): Button { return new MobileButton(label); }
createCheckbox(label: string): Checkbox { return new MobileCheckbox(label); }
createDialog(): Dialog { return new MobileDialog(); }
}
// Cliente usa cualquier factory uniformemente
class SettingsScreen {
private button: Button;
private checkbox: Checkbox;
private dialog: Dialog;
constructor(factory: UIFactory) {
this.button = factory.createButton('Save');
this.checkbox = factory.createCheckbox('Dark Mode');
this.dialog = factory.createDialog();
this.dialog.setTitle('Confirm');
}
render(): string {
return `${this.button.render()} ${this.checkbox.render()}`;
}
}
// Uso
const webScreen = new SettingsScreen(new WebFactory());
const mobileScreen = new SettingsScreen(new MobileFactory());
Cómo Funciona
- Abstract Factory declara métodos de creación para cada tipo de producto
- Concrete Factory implementa creación para una familia específica
- Abstract Products declaran interfaces para tipos de producto
- Concrete Products implementan una variante para una familia
- Cliente usa solo interfaces abstractas; la factory concreta determina la familia
Consecuencias y Trade-offs
La garantía de consistencia es el punto — y también el coste. Una vez que el código cliente tiene una factory, todo objeto que construye viene de la misma familia, así que mezclar plataformas deja de ser posible por construcción. Eso vale dinero real en un equipo de design system: nadie vuelve a reportar un checkbox web dentro de un diálogo nativo. El reverso es la rigidez. Agregar un nuevo tipo de producto — digamos, un date picker — significa tocar la interfaz de la factory abstracta más cada factory concreta. Con tres plataformas son cuatro ediciones por una feature; con diez, un impuesto de mantenimiento que no se va.
La estrategia de selección importa más que las clases. El wiring en compile-time (contenedor DI, build variants) es simple y falla rápido, pero ata la familia a un deploy. La selección en runtime (detectPlatform() al arrancar, o por request para contextos web+embed) cuesta indirección pero deja que un binario sirva varias familias. Elige runtime cuando el mismo bundle va a plataformas distintas; compile-time cuando cada plataforma ya tiene su propio artefacto.
El borde de la familia es una decisión de diseño, no un detalle de implementación. “Todo lo de iOS” es demasiado amplio — acopla componentes que evolucionan a distintas velocidades. “Exactamente los widgets de formulario” es angosto pero estable. Si un tipo de producto cambia por razones ajenas al split de plataformas, probablemente no pertenece a la familia. En duda, empieza con la familia más chica que muestre el problema de mezcla y crécela solo cuando un segundo producto real necesite la garantía.
Una estimación práctica: cada tipo de producto es una interfaz más una implementación por familia. Un design system de 5 productos y 3 plataformas son 15 clases concretas más 5 interfaces — manejable. Uno de 15 productos y 3 plataformas son 45 clases que mantienes para siempre. Abstract Factory gana su complejidad cuando la familia es chica, las plataformas son reales, y el bug de mezcla es caro. Cuando las tres no se cumplen, un Factory Method o un theme object hacen el mismo trabajo con una fracción de la superficie.
Consideraciones de Producción
- Usa frameworks de inyección de dependencias para seleccionar la factory en runtime
- Abstract Factory funciona bien con Bridge cuando las familias también necesitan renderizado específico de plataforma
- Considera registros de factory para arquitecturas basadas en plugins
- Código complementario: el repo companion de abstract-factory-cross-platform empaqueta cada snippet de esta página como archivos TypeScript ejecutables.
Errores Comunes
- Devolver tipos de producto concretos desde los métodos de la factory — si
createButtondevuelveWebButtonen lugar deButton, los clientes se acoplan a la clase concreta y la garantía de familia se evapora. - Dejar que el código cliente construya productos directamente — un
new WebButton()junto a un diálogo creado por factory reintroduce justo la mezcla que el patrón existe para prevenir. - Detectar la plataforma dentro del cliente —
if (isMobile)esparcido por las vistas derrota el patrón; detecta una vez en el bootstrap e inyecta la factory. - Testear solo los productos, no la factory — una
MobileFactoryque devuelveWebCheckboxcompila bien y despacha el bug; afirma la familia por cada factory.
Técnicas Avanzadas
Composición de factory con inyección de dependencias
Combina Abstract Factory con contenedores DI para selección de factory en runtime:
// factory/FactoryRegistry.ts
interface FactoryRegistry {
register(key: string, factory: UIFactory): void;
get(key: string): UIFactory;
}
class UIFactoryRegistry implements FactoryRegistry {
private factories = new Map<string, UIFactory>();
register(key: string, factory: UIFactory): void {
this.factories.set(key, factory);
}
get(key: string): UIFactory {
const factory = this.factories.get(key);
if (!factory) throw new Error(`Factory not found: ${key}`);
return factory;
}
}
// Uso con DI
const registry = new UIFactoryRegistry();
registry.register('web', new WebFactory());
registry.register('mobile', new MobileFactory());
// Selección en runtime basada en ambiente
const platform = detectPlatform(); // 'web' | 'mobile'
const factory = registry.get(platform);
const screen = new SettingsScreen(factory);
Carga dinámica de factories con plugins
Carga factories dinámicamente basadas en configuración o plugins:
// factory/DynamicFactoryLoader.ts
interface FactoryConfig {
type: string;
module: string;
className: string;
}
async function loadFactory(config: FactoryConfig): Promise<UIFactory> {
const module = await import(config.module);
const FactoryClass = module[config.className];
return new FactoryClass();
}
// Selección de factory basada en config
const config: FactoryConfig = {
type: 'ios',
module: './factories/iOS',
className: 'IOSFactory'
};
const factory = await loadFactory(config);
Factory con creación de productos parametrizada
Pasa configuración a factories para instanciación flexible de productos:
interface UIFactoryConfig {
theme: 'light' | 'dark';
locale: string;
accessibility: boolean;
}
interface ConfigurableUIFactory extends UIFactory {
setConfig(config: UIFactoryConfig): void;
}
class WebFactory implements ConfigurableUIFactory {
private config: UIFactoryConfig = {
theme: 'light',
locale: 'en',
accessibility: false
};
setConfig(config: UIFactoryConfig): void {
this.config = { ...this.config, ...config };
}
createButton(label: string): Button {
return new WebButton(label, this.config);
}
}
class WebButton implements Button {
constructor(
private label: string,
private config: UIFactoryConfig
) {}
render(): string {
const themeClass = this.config.theme === 'dark' ? 'dark-mode' : 'light-mode';
return `<button class="${themeClass}">${this.label}</button>`;
}
onClick(handler: () => void): void { /* attach DOM listener */ }
}
Encadenamiento de métodos de factory para ensamblaje complejo de productos
Encadena métodos de factory para construir productos complejos desde múltiples componentes:
interface ComplexUIFactory extends UIFactory {
createForm(): Form;
createFormField(type: 'text' | 'number' | 'email'): FormField;
createValidator(type: 'required' | 'email' | 'minLength'): Validator;
}
class WebFormFactory implements ComplexUIFactory {
createButton(label: string): Button { /* ... */ }
createCheckbox(label: string): Checkbox { /* ... */ }
createDialog(): Dialog { /* ... */ }
createForm(): Form {
return new WebForm();
}
createFormField(type: string): FormField {
return new WebFormField(type);
}
createValidator(type: string): Validator {
return new WebValidator(type);
}
}
// Cliente construye formulario complejo usando cadena de factory
const factory = new WebFormFactory();
const form = factory.createForm();
form.addField(factory.createFormField('text'));
form.addField(factory.createFormField('email'));
form.addValidator(factory.createValidator('required'));
Inicialización lazy de factories con proxies
Usa proxies para diferir la instanciación de factory hasta el primer uso:
class LazyUIFactory implements UIFactory {
private factory: UIFactory | null = null;
private factoryFactory: () => UIFactory;
constructor(factoryFactory: () => UIFactory) {
this.factoryFactory = factoryFactory;
}
private getFactory(): UIFactory {
if (!this.factory) {
this.factory = this.factoryFactory();
}
return this.factory;
}
createButton(label: string): Button {
return this.getFactory().createButton(label);
}
createCheckbox(label: string): Checkbox {
return this.getFactory().createCheckbox(label);
}
createDialog(): Dialog {
return this.getFactory().createDialog();
}
}
// Inicialización lazy
const lazyFactory = new LazyUIFactory(() => {
console.log('Initializing WebFactory...');
return new WebFactory();
});
// Factory no inicializada hasta primera llamada de método
const button = lazyFactory.createButton('Click me');
Mejores Prácticas
- Mantén la familia chica y estable. Cada tipo de producto que agregas cuesta un cambio de interfaz en todas las factories concretas — un date picker son cuatro ediciones en un sistema de tres plataformas. Familias de 3-7 productos envejecen bien; de 15 se vuelven el problema que venían a resolver.
- Inyecta la factory, nunca detectes la plataforma dos veces. El cliente recibe una
UIFactoryen su constructor. La detección de plataforma pasa una vez, en el bootstrap — esparcirif (platform === 'ios')por la app derrota el patrón. - Testea las factories, no solo los productos. Afirma que
WebFactory.createCheckbox()devuelve unWebCheckbox. Una factory que devuelve la familia equivocada compila bien y despacha el bug. - Deja que los productos varíen libremente dentro de la familia. Un
MaterialButtony unIOSButtonpueden diferir muchísimo en markup y comportamiento — el contrato es la interfaz, no el interior. La factory es lo que mantiene las variantes compatibles. - Versiona la familia, no las piezas. Cuando el design system sube de versión (widgets v1 -> v2), despacha una
V2UIFactoryen vez de mutar cada producto. Las pantallas viejas se quedan con la familia vieja, las nuevas optan por entrar.
Preguntas frecuentes
¿En qué se diferencia de Factory Method?
Factory Method crea un producto a través de un método sobreescribible — cada subclase elige su variante. Abstract Factory crea una familia completa de productos relacionados a través de un objeto inyectado. Usa Factory Method para un tipo intercambiable; usa Abstract Factory cuando varios tipos deben mantenerse compatibles entre sí.
¿Cuándo debería evitar Abstract Factory?
Cuándo la familia tiene uno o dos productos, cuando las diferencias son cosméticas (un theme object cubre colores), o cuando solo soportas una plataforma. El patrón rinde solo si el bug de mezcla es real y caro — si no, son interfaces que nadie lee.
¿Puedo usar Abstract Factory con frameworks de inyección de dependencias?
Es la forma limpia de hacerlo. Bindea la factory concreta en la config de tu contenedor (container.bind<UIFactory>(WebFactory)), inyecta la interfaz en todas partes, y cambia de familia rebindeando — sin cambios de código en los consumidores.
¿Cómo manejo agregar un nuevo tipo de producto a una familia de factory existente?
Espera tocar cada factory concreta — es el coste conocido del patrón. Agrega el método a la interfaz abstracta, impleméntalo en cada clase concreta, y escribe un compile check o test por familia. Si agregas tipos de producto todos los meses, la familia probablemente es demasiado amplia.
¿Cómo se compara Abstract Factory con el patrón Builder?
Builder ensambla un objeto complejo paso a paso — un producto, muchas partes. Abstract Factory produce familias de productos listos en una llamada cada uno. Componen bien: una factory puede entregarte builders para los productos que necesitan ensamblaje.
¿Debería usar Abstract Factory para cambio de tema simple?
Casi nunca. Colores, espaciado y tipografía son datos — un theme object o variables CSS los manejan sin clases nuevas. Abstract Factory encaja cuando el comportamiento difiere por plataforma, no solo la apariencia: markup distinto, semántica de interacción distinta, requisitos de a11y distintos.
¿Cómo testeo código que usa Abstract Factory?
Dos niveles: tests por factory afirmando que cada una devuelve los productos de su propia familia, y una UIFactory fake en los tests del cliente para no arrastrar widgets reales a los unit tests. Lo segundo es la recompensa — testear contra la interfaz es exactamente lo que compra el patrón.
Recursos Relacionados
Patrón Abstract Factory
Crea familias de objetos relacionados sin especificar sus clases concretas. Patrón de diseño creacional para familias de objetos consistentes.
RecipeCrear Objetos Flexiblemente con el Factory Pattern
Cómo usar factory methods, abstract factories y containers de inyección de dependencias para desacoplar creación de objetos de su uso y mejorar testeabilidad.
PatternPatrón Bridge para Desacoplar Componentes UI de Temas
Separa la abstracción de su implementación para que ambas varien de forma independiente: el patrón Bridge aplicado a temas UI y motores de renderizado.
PatternDependency Injection Container en TypeScript
Construye un DI container liviano que resuelve dependencias de clases automaticamente, habilitando aplicaciones testeables y debilmente acopladas sin frameworks pesados
RecipeInyección de Dependencias: Contenedores en 4 Lenguajes
Implementa inyección de dependencias para escribir código testeable, desacoplado y mantenible en múltiples lenguajes y frameworks.