Los patrones de diseno son un catalogo de soluciones reutilizables en el diseno de software. Han pasado 30 anos desde el libro GoF (Gang of Four) de 1994, y aunque la esencia de los patrones no ha cambiado, sus metodos de aplicacion han evolucionado significativamente. En este articulo, explicaremos el uso practico de patrones en entornos de desarrollo modernos.
Clasificacion de patrones de diseno
flowchart TB
subgraph DP["Patrones de diseno"]
subgraph Creational["Patrones creacionales"]
C1["Singleton"]
C2["Factory Method"]
C3["Abstract Factory"]
C4["Builder"]
C5["Prototype"]
end
subgraph Structural["Patrones estructurales"]
S1["Adapter"]
S2["Bridge"]
S3["Composite"]
S4["Decorator"]
S5["Facade"]
S6["Flyweight"]
S7["Proxy"]
end
subgraph Behavioral["Patrones de comportamiento"]
B1["Strategy"]
B2["Observer"]
B3["Command"]
B4["State"]
B5["Template Method"]
B6["Iterator"]
B7["Mediator"]
B8["Memento"]
B9["Visitor"]
B10["Chain of Resp."]
end
end
Patrones creacionales
Patron Factory Method
Delega la creacion de objetos a subclases, separando la logica de creacion.
// Implementacion moderna de Factory Method en TypeScript
// Interfaz de producto
interface Notification {
send(message: string): Promise<void>;
}
// Productos concretos
class EmailNotification implements Notification {
constructor(private email: string) {}
async send(message: string): Promise<void> {
console.log(`Email to ${this.email}: ${message}`);
// Logica real de envio de email
}
}
class SlackNotification implements Notification {
constructor(private webhookUrl: string) {}
async send(message: string): Promise<void> {
console.log(`Slack webhook: ${message}`);
// Llamada a API de Slack
}
}
class SMSNotification implements Notification {
constructor(private phoneNumber: string) {}
async send(message: string): Promise<void> {
console.log(`SMS to ${this.phoneNumber}: ${message}`);
// Llamada a API de envio de SMS
}
}
// Factory (basada en funciones - enfoque moderno)
type NotificationType = 'email' | 'slack' | 'sms';
interface NotificationConfig {
type: NotificationType;
email?: string;
webhookUrl?: string;
phoneNumber?: string;
}
function createNotification(config: NotificationConfig): Notification {
switch (config.type) {
case 'email':
if (!config.email) throw new Error('Email required');
return new EmailNotification(config.email);
case 'slack':
if (!config.webhookUrl) throw new Error('Webhook URL required');
return new SlackNotification(config.webhookUrl);
case 'sms':
if (!config.phoneNumber) throw new Error('Phone number required');
return new SMSNotification(config.phoneNumber);
default:
throw new Error(`Unknown notification type: ${config.type}`);
}
}
// Ejemplo de uso
const notification = createNotification({
type: 'email',
email: 'user@example.com'
});
await notification.send('Hello!');
Patron Builder
Construye objetos complejos paso a paso.
// Patron Builder - Implementacion Fluent API
interface HttpRequestConfig {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
headers: Record<string, string>;
body?: unknown;
timeout: number;
retries: number;
}
class HttpRequestBuilder {
private config: Partial<HttpRequestConfig> = {
method: 'GET',
headers: {},
timeout: 30000,
retries: 0,
};
url(url: string): this {
this.config.url = url;
return this;
}
method(method: HttpRequestConfig['method']): this {
this.config.method = method;
return this;
}
header(key: string, value: string): this {
this.config.headers = {
...this.config.headers,
[key]: value,
};
return this;
}
authorization(token: string): this {
return this.header('Authorization', `Bearer ${token}`);
}
contentType(type: string): this {
return this.header('Content-Type', type);
}
json(data: unknown): this {
this.config.body = data;
return this.contentType('application/json');
}
timeout(ms: number): this {
this.config.timeout = ms;
return this;
}
retries(count: number): this {
this.config.retries = count;
return this;
}
build(): HttpRequestConfig {
if (!this.config.url) {
throw new Error('URL is required');
}
return this.config as HttpRequestConfig;
}
// Metodo conveniente - ejecucion directa
async execute<T>(): Promise<T> {
const config = this.build();
// Logica real de ejecucion de fetch
const response = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.body ? JSON.stringify(config.body) : undefined,
});
return response.json();
}
}
// Ejemplo de uso
const response = await new HttpRequestBuilder()
.url('https://api.example.com/users')
.method('POST')
.authorization('my-token')
.json({ name: 'John', email: 'john@example.com' })
.timeout(5000)
.retries(3)
.execute<{ id: string }>();
Patron Singleton (alternativas modernas)
Evita el Singleton tradicional y utiliza contenedores DI o scope de modulo.
// ❌ Singleton tradicional (evitar)
class LegacySingleton {
private static instance: LegacySingleton;
private constructor() {}
static getInstance(): LegacySingleton {
if (!LegacySingleton.instance) {
LegacySingleton.instance = new LegacySingleton();
}
return LegacySingleton.instance;
}
}
// ✅ Singleton con scope de modulo
// database.ts
class DatabaseConnection {
constructor(private connectionString: string) {}
async query<T>(sql: string): Promise<T[]> {
// Ejecucion de consulta
return [];
}
}
// Exportar a nivel de modulo
export const db = new DatabaseConnection(process.env.DATABASE_URL!);
// ✅ Usando contenedor DI (recomendado)
// container.ts
import { Container } from 'inversify';
const container = new Container();
container.bind<DatabaseConnection>('Database')
.to(DatabaseConnection)
.inSingletonScope();
export { container };
Patrones estructurales
Patron Adapter
Hace de puente entre interfaces incompatibles.
// Adaptar una API legacy a una nueva interfaz
// Sistema legacy existente
interface LegacyPaymentSystem {
processPayment(
amount: number,
cardNumber: string,
expiry: string,
cvv: string
): boolean;
}
class LegacyStripePayment implements LegacyPaymentSystem {
processPayment(
amount: number,
cardNumber: string,
expiry: string,
cvv: string
): boolean {
console.log('Processing via legacy Stripe...');
return true;
}
}
// Nueva interfaz
interface PaymentGateway {
charge(payment: PaymentDetails): Promise<PaymentResult>;
}
interface PaymentDetails {
amount: number;
currency: string;
card: {
number: string;
expiryMonth: number;
expiryYear: number;
cvc: string;
};
}
interface PaymentResult {
success: boolean;
transactionId: string;
error?: string;
}
// Adapter
class LegacyPaymentAdapter implements PaymentGateway {
constructor(private legacySystem: LegacyPaymentSystem) {}
async charge(payment: PaymentDetails): Promise<PaymentResult> {
const expiry = `${payment.card.expiryMonth}/${payment.card.expiryYear}`;
const success = this.legacySystem.processPayment(
payment.amount,
payment.card.number,
expiry,
payment.card.cvc
);
return {
success,
transactionId: success ? crypto.randomUUID() : '',
error: success ? undefined : 'Payment failed',
};
}
}
// Ejemplo de uso
const legacyStripe = new LegacyStripePayment();
const paymentGateway: PaymentGateway = new LegacyPaymentAdapter(legacyStripe);
const result = await paymentGateway.charge({
amount: 1000,
currency: 'JPY',
card: {
number: '4242424242424242',
expiryMonth: 12,
expiryYear: 2025,
cvc: '123',
},
});
Patron Decorator
Agrega funcionalidad dinamicamente a objetos existentes.
// Implementacion con decoradores de TypeScript
// Decorador de metodo - salida de log
function Log(
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
console.log(`[${propertyKey}] Called with:`, args);
const start = performance.now();
try {
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
console.log(`[${propertyKey}] Returned:`, result, `(${duration}ms)`);
return result;
} catch (error) {
console.error(`[${propertyKey}] Error:`, error);
throw error;
}
};
return descriptor;
}
// Decorador de cache
function Cache(ttlMs: number = 60000) {
const cache = new Map<string, { value: unknown; expiry: number }>();
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const key = JSON.stringify(args);
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
console.log(`[Cache Hit] ${propertyKey}`);
return cached.value;
}
const result = await originalMethod.apply(this, args);
cache.set(key, { value: result, expiry: Date.now() + ttlMs });
return result;
};
return descriptor;
};
}
// Decorador de reintentos
function Retry(maxAttempts: number = 3, delayMs: number = 1000) {
return function (
target: object,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
console.warn(`[Retry] Attempt ${attempt}/${maxAttempts} failed`);
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
}
throw lastError!;
};
return descriptor;
};
}
// Aplicacion de decoradores
class UserService {
@Log
@Cache(30000) // Cache de 30 segundos
@Retry(3, 1000)
async getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
Patron Proxy
Controla el acceso a objetos.
// Implementacion usando ES6 Proxy
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
// Proxy de validacion
function createValidatedUser(user: User): User {
return new Proxy(user, {
set(target, property, value) {
if (property === 'email') {
if (typeof value !== 'string' || !value.includes('@')) {
throw new Error('Invalid email format');
}
}
if (property === 'role') {
if (!['admin', 'user'].includes(value)) {
throw new Error('Invalid role');
}
}
return Reflect.set(target, property, value);
},
});
}
// Proxy de carga diferida
function createLazyLoader<T extends object>(
loader: () => Promise<T>
): T {
let instance: T | null = null;
let loading: Promise<T> | null = null;
return new Proxy({} as T, {
get(target, property) {
if (!instance) {
if (!loading) {
loading = loader().then(loaded => {
instance = loaded;
return loaded;
});
}
// Elegir entre devolver promesa o bloquear
return loading.then(inst => (inst as any)[property]);
}
return (instance as any)[property];
},
});
}
// Proxy de control de acceso
function createSecureObject<T extends object>(
obj: T,
currentUser: User
): T {
return new Proxy(obj, {
get(target, property) {
const value = Reflect.get(target, property);
// Propiedades solo para administradores
const adminOnlyProps = ['password', 'secretKey', 'apiToken'];
if (adminOnlyProps.includes(String(property))) {
if (currentUser.role !== 'admin') {
throw new Error('Access denied: Admin only');
}
}
return value;
},
set(target, property, value) {
if (currentUser.role !== 'admin') {
throw new Error('Access denied: Read only');
}
return Reflect.set(target, property, value);
},
});
}
Patrones de comportamiento
Patron Strategy
Encapsula algoritmos y los hace intercambiables.
// Estrategia de calculo de precios
interface PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number;
getName(): string;
}
class RegularPricing implements PricingStrategy {
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity;
}
getName(): string {
return 'Regular';
}
}
class BulkPricing implements PricingStrategy {
constructor(private discountThreshold: number, private discountRate: number) {}
calculatePrice(basePrice: number, quantity: number): number {
if (quantity >= this.discountThreshold) {
return basePrice * quantity * (1 - this.discountRate);
}
return basePrice * quantity;
}
getName(): string {
return `Mayorista (${this.discountRate * 100}% de descuento para ${this.discountThreshold}+)`;
}
}
class SubscriberPricing implements PricingStrategy {
constructor(private memberDiscountRate: number) {}
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity * (1 - this.memberDiscountRate);
}
getName(): string {
return `Suscriptor (${this.memberDiscountRate * 100}% de descuento)`;
}
}
class SeasonalPricing implements PricingStrategy {
constructor(
private seasonalMultiplier: number,
private seasonName: string
) {}
calculatePrice(basePrice: number, quantity: number): number {
return basePrice * quantity * this.seasonalMultiplier;
}
getName(): string {
return `Temporada - ${this.seasonName}`;
}
}
// Contexto
class ShoppingCart {
private items: Array<{ name: string; price: number; quantity: number }> = [];
private pricingStrategy: PricingStrategy = new RegularPricing();
addItem(name: string, price: number, quantity: number): void {
this.items.push({ name, price, quantity });
}
setPricingStrategy(strategy: PricingStrategy): void {
this.pricingStrategy = strategy;
}
calculateTotal(): number {
return this.items.reduce((total, item) => {
return total + this.pricingStrategy.calculatePrice(item.price, item.quantity);
}, 0);
}
getReceipt(): string {
const lines = this.items.map(item => {
const subtotal = this.pricingStrategy.calculatePrice(item.price, item.quantity);
return `${item.name} x${item.quantity}: ¥${subtotal}`;
});
return [
`Precio: ${this.pricingStrategy.getName()}`,
'---',
...lines,
'---',
`Total: ¥${this.calculateTotal()}`,
].join('\n');
}
}
// Ejemplo de uso
const cart = new ShoppingCart();
cart.addItem('Widget', 1000, 5);
cart.addItem('Gadget', 2000, 3);
console.log(cart.getReceipt());
// Precio: Regular
// Total: ¥11000
cart.setPricingStrategy(new BulkPricing(3, 0.15));
console.log(cart.getReceipt());
// Precio: Mayorista (15% de descuento para 3+)
// Total: ¥9350
Patron Observer
Define una dependencia uno-a-muchos entre objetos.
// Implementacion Observer con tipado seguro en TypeScript
type EventMap = {
userCreated: { id: string; email: string };
userUpdated: { id: string; changes: Partial<User> };
userDeleted: { id: string };
orderPlaced: { orderId: string; userId: string; total: number };
};
type EventKey = keyof EventMap;
type EventHandler<K extends EventKey> = (event: EventMap[K]) => void | Promise<void>;
class TypedEventEmitter {
private handlers = new Map<EventKey, Set<EventHandler<any>>>();
on<K extends EventKey>(event: K, handler: EventHandler<K>): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
// Devuelve funcion de cancelacion de suscripcion
return () => this.off(event, handler);
}
off<K extends EventKey>(event: K, handler: EventHandler<K>): void {
this.handlers.get(event)?.delete(handler);
}
async emit<K extends EventKey>(event: K, data: EventMap[K]): Promise<void> {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers) return;
const promises = Array.from(eventHandlers).map(handler =>
Promise.resolve(handler(data))
);
await Promise.all(promises);
}
once<K extends EventKey>(event: K, handler: EventHandler<K>): () => void {
const wrappedHandler: EventHandler<K> = async (data) => {
this.off(event, wrappedHandler);
await handler(data);
};
return this.on(event, wrappedHandler);
}
}
// Ejemplo de uso
const eventBus = new TypedEventEmitter();
// Servicio de envio de emails
eventBus.on('userCreated', async ({ email }) => {
console.log(`Sending welcome email to ${email}`);
});
// Servicio de analiticas
eventBus.on('userCreated', ({ id }) => {
console.log(`Tracking user creation: ${id}`);
});
// Procesamiento de pedidos
eventBus.on('orderPlaced', async ({ orderId, userId, total }) => {
console.log(`Processing order ${orderId} for user ${userId}: ¥${total}`);
});
// Disparar evento
await eventBus.emit('userCreated', {
id: 'user-123',
email: 'user@example.com',
});
Patron Command
Encapsula operaciones como objetos.
// Implementacion de Command con funcionalidad Undo/Redo
interface Command {
execute(): Promise<void>;
undo(): Promise<void>;
getDescription(): string;
}
// Ejemplo de comandos para editor de texto
class TextEditor {
private content: string = '';
getContent(): string {
return this.content;
}
setContent(content: string): void {
this.content = content;
}
insertAt(position: number, text: string): void {
this.content =
this.content.slice(0, position) + text + this.content.slice(position);
}
deleteRange(start: number, end: number): string {
const deleted = this.content.slice(start, end);
this.content = this.content.slice(0, start) + this.content.slice(end);
return deleted;
}
}
class InsertTextCommand implements Command {
constructor(
private editor: TextEditor,
private position: number,
private text: string
) {}
async execute(): Promise<void> {
this.editor.insertAt(this.position, this.text);
}
async undo(): Promise<void> {
this.editor.deleteRange(this.position, this.position + this.text.length);
}
getDescription(): string {
return `Insertar "${this.text}" en posicion ${this.position}`;
}
}
class DeleteTextCommand implements Command {
private deletedText: string = '';
constructor(
private editor: TextEditor,
private start: number,
private end: number
) {}
async execute(): Promise<void> {
this.deletedText = this.editor.deleteRange(this.start, this.end);
}
async undo(): Promise<void> {
this.editor.insertAt(this.start, this.deletedText);
}
getDescription(): string {
return `Eliminar desde ${this.start} hasta ${this.end}`;
}
}
// Gestor de comandos (Invoker)
class CommandManager {
private history: Command[] = [];
private redoStack: Command[] = [];
async execute(command: Command): Promise<void> {
await command.execute();
this.history.push(command);
this.redoStack = []; // Limpiar pila de redo al ejecutar nuevo comando
}
async undo(): Promise<boolean> {
const command = this.history.pop();
if (!command) return false;
await command.undo();
this.redoStack.push(command);
return true;
}
async redo(): Promise<boolean> {
const command = this.redoStack.pop();
if (!command) return false;
await command.execute();
this.history.push(command);
return true;
}
getHistory(): string[] {
return this.history.map(cmd => cmd.getDescription());
}
}
// Ejemplo de uso
const editor = new TextEditor();
const manager = new CommandManager();
await manager.execute(new InsertTextCommand(editor, 0, 'Hello '));
await manager.execute(new InsertTextCommand(editor, 6, 'World!'));
console.log(editor.getContent()); // "Hello World!"
await manager.undo();
console.log(editor.getContent()); // "Hello "
await manager.redo();
console.log(editor.getContent()); // "Hello World!"
Patrones de arquitectura modernos
Patron Repository
Abstrae la logica de acceso a datos.
// Patron Repository con TypeScript
interface Entity {
id: string;
}
interface Repository<T extends Entity> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
findBy(criteria: Partial<T>): Promise<T[]>;
save(entity: T): Promise<T>;
delete(id: string): Promise<boolean>;
}
interface User extends Entity {
id: string;
email: string;
name: string;
createdAt: Date;
}
// Implementacion en memoria (para pruebas)
class InMemoryUserRepository implements Repository<User> {
private users: Map<string, User> = new Map();
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null;
}
async findAll(): Promise<User[]> {
return Array.from(this.users.values());
}
async findBy(criteria: Partial<User>): Promise<User[]> {
return Array.from(this.users.values()).filter(user =>
Object.entries(criteria).every(
([key, value]) => user[key as keyof User] === value
)
);
}
async save(entity: User): Promise<User> {
this.users.set(entity.id, entity);
return entity;
}
async delete(id: string): Promise<boolean> {
return this.users.delete(id);
}
}
// Implementacion con Prisma (para produccion)
class PrismaUserRepository implements Repository<User> {
constructor(private prisma: PrismaClient) {}
async findById(id: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { id } });
}
async findAll(): Promise<User[]> {
return this.prisma.user.findMany();
}
async findBy(criteria: Partial<User>): Promise<User[]> {
return this.prisma.user.findMany({ where: criteria });
}
async save(entity: User): Promise<User> {
return this.prisma.user.upsert({
where: { id: entity.id },
update: entity,
create: entity,
});
}
async delete(id: string): Promise<boolean> {
try {
await this.prisma.user.delete({ where: { id } });
return true;
} catch {
return false;
}
}
}
Patron Unit of Work
Rastrea y gestiona cambios dentro de una transaccion.
// Implementacion de Unit of Work
interface UnitOfWork {
begin(): Promise<void>;
commit(): Promise<void>;
rollback(): Promise<void>;
userRepository: Repository<User>;
orderRepository: Repository<Order>;
}
class PrismaUnitOfWork implements UnitOfWork {
private transaction: Prisma.TransactionClient | null = null;
private _userRepository: Repository<User> | null = null;
private _orderRepository: Repository<Order> | null = null;
constructor(private prisma: PrismaClient) {}
async begin(): Promise<void> {
// Transaccion interactiva de Prisma
return new Promise((resolve) => {
this.prisma.$transaction(async (tx) => {
this.transaction = tx;
resolve();
// La transaccion se mantiene hasta commit/rollback
});
});
}
get userRepository(): Repository<User> {
if (!this._userRepository) {
this._userRepository = new PrismaUserRepository(
this.transaction || this.prisma
);
}
return this._userRepository;
}
get orderRepository(): Repository<Order> {
if (!this._orderRepository) {
this._orderRepository = new PrismaOrderRepository(
this.transaction || this.prisma
);
}
return this._orderRepository;
}
async commit(): Promise<void> {
// La transaccion de Prisma hace commit automatico
this.transaction = null;
}
async rollback(): Promise<void> {
throw new Error('Rollback requested');
}
}
// Ejemplo de uso
async function createOrderWithUser(uow: UnitOfWork) {
await uow.begin();
try {
const user = await uow.userRepository.save({
id: crypto.randomUUID(),
email: 'new@example.com',
name: 'New User',
createdAt: new Date(),
});
const order = await uow.orderRepository.save({
id: crypto.randomUUID(),
userId: user.id,
total: 5000,
status: 'pending',
});
await uow.commit();
return { user, order };
} catch (error) {
await uow.rollback();
throw error;
}
}
Relacion con los principios SOLID
flowchart LR
subgraph SOLID["Principios SOLID"]
S["S - Principio de responsabilidad unica (SRP)"]
O["O - Principio abierto/cerrado (OCP)"]
L["L - Principio de sustitucion de Liskov (LSP)"]
I["I - Principio de segregacion de interfaces (ISP)"]
D["D - Principio de inversion de dependencias (DIP)"]
end
subgraph Patterns["Patrones relacionados"]
P1["Factory, Strategy, Command"]
P2["Strategy, Decorator, Template Method"]
P3["Factory Method, Abstract Factory"]
P4["Adapter, Facade"]
P5["Repository, Dependency Injection"]
end
S --> P1
O --> P2
L --> P3
I --> P4
D --> P5
Resumen
Los patrones de diseno son un lenguaje comun para la resolucion de problemas y tambien son utiles para alinear el entendimiento en el desarrollo en equipo.
Guia de seleccion
| Objetivo | Patron recomendado |
|---|---|
| Flexibilidad en creacion de objetos | Factory, Builder |
| Extension de codigo existente | Decorator, Adapter |
| Intercambio de algoritmos | Strategy |
| Orientado a eventos | Observer |
| Deshacer operaciones | Command |
| Abstraccion de acceso a datos | Repository |
Mejores practicas en desarrollo moderno
- Evitar la aplicacion excesiva de patrones - Soluciones simples para problemas simples
- Aprovechar las caracteristicas del lenguaje - Sistema de tipos de TypeScript/Python, decoradores
- Priorizar la testeabilidad - Usar DI para hacer las dependencias inyectables
- Combinar con enfoque funcional - Usar funciones para procesamiento sin estado
Los patrones de diseno son un medio, no un fin. Es importante entender el problema y seleccionar el patron apropiado.