¿Qué son los Patrones de Diseño?
Los patrones de diseño son soluciones reutilizables a problemas que ocurren frecuentemente en el diseño de software. Fueron sistematizados en el libro de GoF (Gang of Four) de 1994.
La importancia de aprender patrones: Evitar reinventar la rueda y tener un vocabulario común entre desarrolladores.
Patrones Creacionales
Singleton
Garantiza que una clase tenga solo una instancia.
// Singleton en JavaScript
class Database {
static #instance = null;
constructor() {
if (Database.#instance) {
return Database.#instance;
}
this.connection = this.connect();
Database.#instance = this;
}
connect() {
console.log('Database connected');
return { /* connection */ };
}
static getInstance() {
if (!Database.#instance) {
Database.#instance = new Database();
}
return Database.#instance;
}
}
// Uso
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true
Casos de uso: Conexión a base de datos, funcionalidad de logging, gestión de configuración
Factory
Encapsula la creación de objetos.
// Factory para crear notificaciones
class NotificationFactory {
static create(type, message) {
switch (type) {
case 'email':
return new EmailNotification(message);
case 'sms':
return new SMSNotification(message);
case 'push':
return new PushNotification(message);
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
}
// Uso
const notification = NotificationFactory.create('email', 'Hello!');
notification.send();
Casos de uso: Creación de objetos según condiciones, separación de dependencias
Builder
Construye objetos complejos paso a paso.
class QueryBuilder {
constructor() {
this.query = { select: '*', from: '', where: [], orderBy: '' };
}
select(fields) {
this.query.select = fields;
return this;
}
from(table) {
this.query.from = table;
return this;
}
where(condition) {
this.query.where.push(condition);
return this;
}
orderBy(field) {
this.query.orderBy = field;
return this;
}
build() {
let sql = `SELECT ${this.query.select} FROM ${this.query.from}`;
if (this.query.where.length > 0) {
sql += ` WHERE ${this.query.where.join(' AND ')}`;
}
if (this.query.orderBy) {
sql += ` ORDER BY ${this.query.orderBy}`;
}
return sql;
}
}
// Uso
const query = new QueryBuilder()
.select('name, email')
.from('users')
.where('status = "active"')
.where('age > 18')
.orderBy('created_at DESC')
.build();
Casos de uso: Construcción de consultas SQL, construcción de solicitudes HTTP, objetos de configuración complejos
Patrones Estructurales
Adapter
Convierte interfaces incompatibles.
// API antigua
class OldPaymentSystem {
processPayment(amount) {
console.log(`Old system: Processing ${amount}`);
return { success: true };
}
}
// Interfaz de la nueva API
class PaymentAdapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
pay(request) {
// Convierte la nueva interfaz al sistema antiguo
const result = this.oldSystem.processPayment(request.amount);
return {
transactionId: `txn_${Date.now()}`,
status: result.success ? 'completed' : 'failed'
};
}
}
// Uso
const adapter = new PaymentAdapter(new OldPaymentSystem());
adapter.pay({ amount: 1000, currency: 'JPY' });
Casos de uso: Integración de sistemas legacy, abstracción de bibliotecas de terceros
Decorator
Añade funcionalidad dinámicamente a objetos.
// Café básico
class Coffee {
cost() { return 300; }
description() { return 'Café'; }
}
// Decoradores
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 50; }
description() { return `${this.coffee.description()} + Leche`; }
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() { return this.coffee.cost() + 20; }
description() { return `${this.coffee.description()} + Azúcar`; }
}
// Uso
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.description()); // Café + Leche + Azúcar
console.log(coffee.cost()); // 370
Casos de uso: Middleware, añadir funcionalidad de logging, añadir caché
Facade
Proporciona una interfaz simple a un subsistema complejo.
// Subsistema complejo
class VideoDecoder { decode(file) { /* ... */ } }
class AudioDecoder { decode(file) { /* ... */ } }
class SubtitleParser { parse(file) { /* ... */ } }
class VideoPlayer { play(video, audio, subtitle) { /* ... */ } }
// Facade
class MediaPlayerFacade {
constructor() {
this.videoDecoder = new VideoDecoder();
this.audioDecoder = new AudioDecoder();
this.subtitleParser = new SubtitleParser();
this.player = new VideoPlayer();
}
playVideo(filename) {
const video = this.videoDecoder.decode(filename);
const audio = this.audioDecoder.decode(filename);
const subtitle = this.subtitleParser.parse(filename);
this.player.play(video, audio, subtitle);
}
}
// Uso (¡Simple!)
const player = new MediaPlayerFacade();
player.playVideo('movie.mp4');
Casos de uso: Wrappers de bibliotecas, simplificación de procesos complejos
Patrones de Comportamiento
Observer
Notifica cambios de estado de un objeto a múltiples objetos.
class EventEmitter {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}
}
// Uso
const store = new EventEmitter();
store.on('userLoggedIn', (user) => {
console.log(`Welcome, ${user.name}!`);
});
store.on('userLoggedIn', (user) => {
analytics.track('login', { userId: user.id });
});
store.emit('userLoggedIn', { id: 1, name: 'Alice' });
Casos de uso: Sistemas de eventos, gestión de estado, programación reactiva
Strategy
Hace intercambiables los algoritmos.
// Estrategias de pago
const paymentStrategies = {
creditCard: (amount) => {
console.log(`Credit card payment: ${amount}`);
return { method: 'creditCard', fee: amount * 0.03 };
},
bankTransfer: (amount) => {
console.log(`Bank transfer: ${amount}`);
return { method: 'bankTransfer', fee: 0 };
},
paypal: (amount) => {
console.log(`PayPal payment: ${amount}`);
return { method: 'paypal', fee: amount * 0.04 };
}
};
class PaymentProcessor {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
pay(amount) {
return this.strategy(amount);
}
}
// Uso
const processor = new PaymentProcessor(paymentStrategies.creditCard);
processor.pay(1000);
processor.setStrategy(paymentStrategies.bankTransfer);
processor.pay(1000);
Casos de uso: Algoritmos de ordenación, cambio de métodos de autenticación, cálculo de tarifas
Command
Encapsula operaciones como objetos.
// Interfaz de comando
class Command {
execute() { throw new Error('Not implemented'); }
undo() { throw new Error('Not implemented'); }
}
// Comando concreto
class AddTextCommand extends Command {
constructor(editor, text) {
super();
this.editor = editor;
this.text = text;
}
execute() {
this.editor.content += this.text;
}
undo() {
this.editor.content = this.editor.content.slice(0, -this.text.length);
}
}
// Ejecutor
class CommandExecutor {
constructor() {
this.history = [];
}
execute(command) {
command.execute();
this.history.push(command);
}
undo() {
const command = this.history.pop();
if (command) {
command.undo();
}
}
}
// Uso
const editor = { content: '' };
const executor = new CommandExecutor();
executor.execute(new AddTextCommand(editor, 'Hello '));
executor.execute(new AddTextCommand(editor, 'World'));
console.log(editor.content); // 'Hello World'
executor.undo();
console.log(editor.content); // 'Hello '
Casos de uso: Funcionalidad Deshacer/Rehacer, transacciones, colas de tareas
Resumen
Los patrones de diseño funcionan como un lenguaje común en el diseño de software y proporcionan soluciones probadas a problemas comunes. Sin embargo, aplicar patrones no es un fin en sí mismo; lo importante es seleccionar el patrón adecuado para cada problema.
← Volver a la lista