Introducción a los Patrones de Diseño - Patrones de diseño comúnmente utilizados

17 min de lectura | 2024.12.27

¿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