Patrones de Diseño Arquitectónicos y Tipado Avanzado en TypeScript
Descubre cómo estructurar aplicaciones robustas aplicando patrones de diseño clásicos como Singleton, Factory, Observer y Strategy combinados con el sistema de tipos avanzados de TypeScript para garantizar máxima seguridad y mantenibilidad.
Introducción a la Arquitectura de Software con TypeScript
El desarrollo de aplicaciones web y de backend a gran escala requiere de una disciplina estricta en la estructuración del código. TypeScript no es simplemente un lenguaje con comprobación estática de tipos; es una herramienta de diseño arquitectónico que nos permite modelar dominios complejos de manera explícita y segura.
Cuando combinamos patrones de diseño con el potente sistema de tipos de TypeScript (como tipos condicionales, mapeados, y genéricos avanzados), obtenemos una arquitectura autodocumentada donde los errores de lógica e integración se detectan en tiempo de compilación y no en producción.
1. El Patrón Strategy con Tipado Estricto
El patrón Strategy nos permite definir una familia de algoritmos, encapsular cada uno de ellos y hacerlos intercambiables. En TypeScript puro, es fácil cometer errores al pasar argumentos genéricos a las estrategias. Vamos a diseñar un motor de cálculo de impuestos y descuentos altamente tipado.
Implementación Base de las Estrategias
Comenzaremos definiendo un contexto genérico donde las estrategias dependen de la estructura de los datos que van a procesar.
interface ItemCarrito {
id: string;
nombre: string;
precio: number;
cantidad: number;
}
interface EstrategiaCalculoTotal {
calcular(items: ItemCarrito[]): number;
}
class DescuentoFijoStrategy implements EstrategiaCalculoTotal {
constructor(private descuento: number) {}
calcular(items: ItemCarrito[]): number {
const total = items.reduce((acc, item) => acc + (item.precio * item.cantidad), 0);
return Math.max(0, total - this.descuento);
}
}
class DescuentoPorcentajeStrategy implements EstrategiaCalculoTotal {
constructor(private porcentaje: number) {}
calcular(items: ItemCarrito[]): number {
const total = items.reduce((acc, item) => acc + (item.precio * item.cantidad), 0);
const factor = (100 - this.porcentaje) / 100;
return total * factor;
}
}
Contexto de Uso
Ahora implementamos el contexto que consume estas estrategias asegurando que no se pueda inyectar una estrategia inválida.
class CarritoCompraContext {
private estrategia: EstrategiaCalculoTotal;
constructor(estrategiaInicial: EstrategiaCalculoTotal) {
this.estrategia = estrategiaInicial;
}
public setEstrategia(nuevaEstrategia: EstrategiaCalculoTotal): void {
this.estrategia = nuevaEstrategia;
}
public calcularTotalFinal(items: ItemCarrito[]): number {
return this.estrategia.calcular(items);
}
}
2. Patrón Factory con Mapeo de Tipos Condicionales
El patrón Factory method se encarga de delegar la creación de objetos a subclases o funciones centralizadas. Utilizando los tipos condicionales y mapeados de TypeScript, podemos crear una factoría fuertemente tipada que devuelva la instancia exacta basada en un discriminador.
Definición de Entidades
interface NotificacionEmail {
tipo: 'email';
destinatario: string;
asunto: string;
cuerpo: string;
}
interface NotificacionSMS {
tipo: 'sms';
telefono: string;
mensaje: string;
}
interface NotificacionPush {
tipo: 'push';
tokenDispositivo: string;
titulo: string;
payload: Record<string, unknown>;
}
type NotificacionPayload = NotificacionEmail | NotificacionSMS | NotificacionPush;
Mapeo de Clases y Creador Genérico
Implementemos un registro dinámico de creadores utilizando un mapa de tipos:
interface CreadorNotificacion<T extends NotificacionPayload> {
enviar(payload: T): void;
}
class EmailService implements CreadorNotificacion<NotificacionEmail> {
enviar(payload: NotificacionEmail): void {
console.log(`Enviando email a ${payload.destinatario}: ${payload.asunto}`);
}
}
class SMSService implements CreadorNotificacion<NotificacionSMS> {
enviar(payload: NotificacionSMS): void {
console.log(`Enviando SMS al teléfono ${payload.telefono}`);
}
}
class NotificacionFactory {
private static registry = new Map<NotificacionPayload['tipo'], CreadorNotificacion<any>>();
public static registrar<T extends NotificacionPayload>(
tipo: T['tipo'],
creador: CreadorNotificacion<T>
): void {
this.registry.set(tipo, creador);
}
public static obtenerCreador<T extends NotificacionPayload>(
tipo: T['tipo']
): CreadorNotificacion<T> {
const servicio = this.registry.get(tipo);
if (!servicio) {
throw new Error(`Servicio para el tipo ${tipo} no encontrado`);
}
return servicio;
}
}
3. Patrón Observer Segregado por Tipos
El patrón Observer es fundamental para la programación reactiva y la gestión de eventos. En TypeScript, solemos caer en el uso de any para los payloads de los eventos. Vamos a resolver esto creando un sistema de observers seguro basado en un mapa de eventos y cargas útiles fuertemente tipadas.
Sistema de Eventos Tipados
type EventosMapa = {
'usuario:login': { userId: string; timestamp: number };
'usuario:logout': { userId: string; razon: string };
'pedido:creado': { pedidoId: string; total: number };
};
type Listener<T> = (payload: T) => void;
class EventBus {
private listeners: {
[K in keyof EventosMapa]?: Array<Listener<EventosMapa[K]>>;
} = {};
public suscribir<K extends keyof EventosMapa>(
evento: K,
listener: Listener<EventosMapa[K]>
): () => void {
if (!this.listeners[evento]) {
this.listeners[evento] = [];
}
this.listeners[evento]!.push(listener);
// Retornamos función para desuscribir
return () => {
this.listeners[evento] = this.listeners[evento]?.filter(l => l !== listener);
};
}
public publicar<K extends keyof EventosMapa>(
evento: K,
payload: EventosMapa[K]
): void {
const callbacks = this.listeners[evento];
if (callbacks) {
callbacks.forEach(callback => callback(payload));
}
}
}
4. Patrón Singleton con Control de Concurrencia e Inyección
El patrón Singleton garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella. Aunque es criticado en programación orientada a objetos moderna por dificultar las pruebas unitarias, sigue siendo muy útil para gestores de conexiones o configuraciones globales.
Singleton Genérico Seguro
abstract class BaseSingleton {
protected static instancia: any = null;
protected constructor() {}
public static obtenerInstancia<T extends BaseSingleton>(this: new () => T): T {
if (!this.instancia) {
this.instancia = new this();
}
return this.instancia;
}
}
class ConfiguracionSistema extends BaseSingleton {
public entorno: 'development' | 'production' = 'development';
public apiTimeout: number = 5000;
private constructor() {
super();
}
}
// Uso:
const config1 = ConfiguracionSistema.obtenerInstancia<ConfiguracionSistema>();
const config2 = ConfiguracionSistema.obtenerInstancia<ConfiguracionSistema>();
console.log(config1 === config2); // true
Comparativa de Patrones y su Aplicación en TypeScript
| Patrón de Diseño | Complejidad de Tipado | Caso de Uso Principal en TypeScript | Ventaja Clave |
|---|---|---|---|
| --- | --- | --- | --- |
| Strategy | Fácil | Algoritmos de negocio intercambiables | Evita condicionales complejos (switch/case) |
| Factory | Intermedio | Creación dinámica de objetos complejos | Desacopla la creación del uso |
| --- | --- | --- | --- |
| Observer | Avanzado | Sistemas reactivos y manejo de eventos | Comunicación desacoplada entre módulos |
| Singleton | Intermedio | Gestión de recursos globales compartidos | Control estricto de instanciación única |
Resumen del Proceso de Diseño Arquitectónico
Preguntas Frecuentes sobre Patrones en TypeScript
¿El uso excesivo de patrones ralentiza la compilación de TypeScript? En proyectos pequeños o medianos el impacto es imperceptible. En monorepos masivos, el uso excesivo de tipos condicionales complejos y recursivos puede incrementar el tiempo del compilador (tsc). Es recomendable mantener los tipos limpios y modulares.
¿Es obligatorio usar clases para aplicar patrones de diseño? No necesariamente. Muchos patrones como Strategy o Factory se pueden implementar utilizando funciones puras y objetos de primera clase, aprovechando el paradigma funcional de JavaScript y TypeScript.
Tutoriales relacionados
- Tipos Utilitarios en TypeScript: Potenciando Tu Código con Mapped Types y Condicionalesadvanced18 min
- Tipado Avanzado de Redux y Redux Toolkit con TypeScript: Una Guía Completaintermediate15 min
- Desarrollo Modular Robusto: Tipado de Entornos de Ejecución con TypeScript y Patrones de Inyección de Dependenciasintermediate20 min
- Tipado de Configuración y Variables de Entorno en TypeScript: Robustez en tu Aplicaciónintermediate15 min
- Tipado de Mixins en TypeScript: Reutilización de Código con Seguridad de Tiposadvanced15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!