¿Alguna vez te has encontrado escribiendo el mismo tipo de código una y otra vez? ¿O has heredado un proyecto donde el código es tan complejo que te cuesta entender qué hace cada parte?

Si alguna de estas situaciones te resulta familiar, déjame presentarte a tus nuevos mejores amigos: los patrones de diseño. Son como recetas probadas que otros desarrolladores han perfeccionado para resolver problemas que tú y yo enfrentamos constantemente.

En esta guía completa, exploraremos los patrones de diseño más importantes en JavaScript moderno, cómo implementarlos con ES6+ y cuándo aplicarlos en tus proyectos. Al final, encontrarás ejercicios prácticos para consolidar tu aprendizaje.

¿Qué son los Patrones de Diseño?

Piensa en los patrones de diseño como el GPS del desarrollo de software. No te dicen exactamente qué teclas presionar, pero sí te muestran el camino más eficiente para llegar a tu destino.

Son soluciones que han sido probadas miles de veces por desarrolladores de todo el mundo. No código para copiar y pegar, sino plantillas conceptuales que moldeas según las necesidades únicas de tu proyecto.

Beneficios de Usar Patrones de Diseño

  • Código mantenible: Facilitan la comprensión y modificación del código
  • Comunicación efectiva: Proporcionan un vocabulario común entre desarrolladores
  • Soluciones probadas: Implementan mejores prácticas de la industria
  • Escalabilidad: Ayudan a crear arquitecturas que crecen con tu aplicación

Patrones de Diseño Creacionales

Los patrones creacionales se enfocan en cómo crear objetos de manera eficiente y flexible.

1. Singleton: una sola instancia para gobernarlas

Su trabajo es simple: asegurar que exista una sola instancia de una clase en toda tu aplicación, como tener un único guardia de seguridad en la entrada principal.

Ejemplo sencillo:

class DatabaseConnection {
  constructor() {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }
    
    this.connection = null;
    DatabaseConnection.instance = this;
  }
  
  connect() {
    if (!this.connection) {
      this.connection = "Conexión establecida";
      console.log("Nueva conexión creada");
    }
    return this.connection;
  }
}

// Uso
const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();

console.log(db1 === db2); // true - misma instancia

Cuándo usar Singleton:

  • Cuando necesitas múltiples instancias con estados diferentes (por ejemplo, conexiones a diferentes bases de datos)
  • Si complica las pruebas unitarias porque crea dependencias globales difíciles de mockear
  • Cuando el estado compartido puede causar efectos secundarios inesperados en aplicaciones concurrentes
  • Si estás tentado a usarlo solo para «ahorrar memoria» sin una razón arquitectural sólida </aside>

Ventajas: Control estricto sobre cómo y cuándo se accede a la instancia

Desventajas: Puede dificultar las pruebas unitarias y crear acoplamiento global

graph TD
    A["Singleton Instance"] 
    B["Reference 1: db1"] --> A
    C["Reference 2: db2"] --> A
    D["Reference 3: db3"] --> A
    E["Reference 4: db4"] --> A
    
    style A fill:#4CAF50,stroke:#2E7D32,stroke-width:3px,color:#fff
    style B fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff
    style C fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff
    style D fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff
    style E fill:#2196F3,stroke:#1565C0,stroke-width:2px,color:#fff

Diagrama mostrando múltiples referencias apuntando a una única instancia

2. Factory: crea objetos sin ensuciar tu código

Supón que tienes una fábrica de vehículos. No necesitas saber todos los detalles técnicos de cómo se ensambla un auto o una moto, solo dices «necesito un auto» y la fábrica se encarga del resto.

Eso es exactamente lo que hace el patrón Factory: crea objetos sin que te preocupes por los detalles internos de su construcción.

Es tu mejor compañero cuando el proceso de creación es complejo o cuando quieres mantener tu código flexible y desacoplado. Por ejemplo:

  • Creación de objetos complejos con múltiples configuraciones
  • Sistema de plugins, donde los tipos de objetos se determinan en tiempo de ejecución
  • Cuando quieres encapsular la lógica de creación

Ejemplo sencillo:

class Car {
  constructor(model) {
    this.model = model;
    this.type = "car";
  }
  
  drive() {
    console.log(`Conduciendo un ${this.model}`);
  }
}

class Motorcycle {
  constructor(model) {
    this.model = model;
    this.type = "motorcycle";
  }
  
  drive() {
    console.log(`Manejando una ${this.model}`);
  }
}

// Factory
class VehicleFactory {
  static createVehicle(type, model) {
    switch(type) {
      case "car":
        return new Car(model);
      case "motorcycle":
        return new Motorcycle(model);
      default:
        throw new Error("Tipo de vehículo no válido");
    }
  }
}

// Uso
const myCar = VehicleFactory.createVehicle("car", "Toyota Corolla");
const myBike = VehicleFactory.createVehicle("motorcycle", "Harley Davidson");

myCar.drive(); // Conduciendo un Toyota Corolla
myBike.drive(); // Manejando una Harley Davidson

Patrones de Diseño Estructurales

Los patrones estructurales se centran en cómo componer objetos y clases para formar estructuras más grandes.

3. Patrón Proxy: Tu guardaespaldas de control de acceso

El patrón Proxy te da un sustituto o marcador de posición para otro objeto, permitiendo controlar el acceso a él. Es útil para añadir funcionalidad adicional como caché, validación o logging.

Ejemplo sencillo:

const person = {
  name: "Juan",
  age: 30,
  email: "juan@example.com"
};

const personProxy = new Proxy(person, {
  get: (target, prop) => {
    console.log(`Accediendo a la propiedad: ${prop}`);
    return target[prop];
  },
  set: (target, prop, value) => {
    if (prop === "age" && typeof value !== "number") {
      throw new Error("La edad debe ser un número");
    }
    console.log(`Modificando ${prop} de ${target[prop]} a ${value}`);
    target[prop] = value;
    return true;
  }
});

// Uso
console.log(personProxy.name); // Accediendo a la propiedad: name
personProxy.age = 31; // Modificando age de 30 a 31
// personProxy.age = "treinta"; // Error: La edad debe ser un número

Cuándo usarlo:

  • Validación de datos
  • Lazy loading de recursos
  • Logging y debugging
  • Control de acceso
Diagrama mostrando un proxy interceptando llamadas entre cliente y objeto real

4. Patrón Module: Organiza tu código como cajones de un armario

El patrón Module permite organizar y encapsular código relacionado en unidades independientes y reutilizables. Con ES6+, esto se logra fácilmente con los módulos nativos.

Ejemplo sencillo:

// mathUtils.js
const privateCounter = 0; // Variable privada

export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// Función privada (no exportada)
const validateNumber = (num) => {
  return typeof num === "number";
};

export const multiply = (a, b) => {
  if (!validateNumber(a) || !validateNumber(b)) {
    throw new Error("Ambos argumentos deben ser números");
  }
  return a * b;
};
// main.js
import { add, multiply } from "./mathUtils.js";

console.log(add(5, 3)); // 8
console.log(multiply(4, 2)); // 8

Cuándo usarlo:

  • Organizar código en archivos separados
  • Evitar contaminar el scope global
  • Crear APIs públicas con implementación privada

5. Patrón Mixin: Comparte responsabilidades sin herencia

El patrón Mixin agrega funcionalidades a objetos o clases sin usar herencia. Es útil cuando quieres compartir comportamiento entre objetos que no tienen una relación jerárquica.

Ejemplo sencillo:

// Mixin con funcionalidad compartida
const canEat = {
  eat(food) {
    console.log(`${this.name} está comiendo ${food}`);
  }
};

const canWalk = {
  walk() {
    console.log(`${this.name} está caminando`);
  }
};

const canSwim = {
  swim() {
    console.log(`${this.name} está nadando`);
  }
};

// Clases base
class Dog {
  constructor(name) {
    this.name = name;
  }
}

class Fish {
  constructor(name) {
    this.name = name;
  }
}

// Aplicar mixins
Object.assign(Dog.prototype, canEat, canWalk);
Object.assign(Fish.prototype, canEat, canSwim);

// Uso
const myDog = new Dog("Rex");
myDog.eat("croquetas"); // Rex está comiendo croquetas
myDog.walk(); // Rex está caminando

const myFish = new Fish("Nemo");
myFish.eat("algas"); // Nemo está comiendo algas
myFish.swim(); // Nemo está nadando

Cuándo usarlo:

  • Compartir funcionalidad entre clases no relacionadas
  • Evitar herencia múltiple
  • Composición sobre herencia

Patrones de Diseño Comportamentales

Los patrones comportamentales se enfocan en la comunicación entre objetos y cómo distribuyen responsabilidades.

6. Patrón Observer

Piensa en YouTube: cuando tu creador de contenido favorito sube un nuevo video, recibes una notificación instantánea. No tienes que estar revisando constantemente su canal.

El patrón Observer funciona exactamente así: establece una relación donde un objeto (el «subject») avisa automáticamente a todos sus suscriptores cuando algo importante sucede.

Es el corazón de cualquier sistema de notificaciones, actualizaciones en tiempo real y aplicaciones reactivas modernas.

Ejemplo sencillo:

class Subject {
  constructor() {
    this.observers = [];
  }
  
  subscribe(observer) {
    this.observers.push(observer);
  }
  
  unsubscribe(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
  
  notify(data) {
    this.observers.forEach(observer => observer.update(data));
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  
  update(data) {
    console.log(`${this.name} recibió la notificación: ${data}`);
  }
}

// Uso
const newsPublisher = new Subject();

const subscriber1 = new Observer("Usuario 1");
const subscriber2 = new Observer("Usuario 2");

newsPublisher.subscribe(subscriber1);
newsPublisher.subscribe(subscriber2);

newsPublisher.notify("¡Nueva noticia disponible!");
// Usuario 1 recibió la notificación: ¡Nueva noticia disponible!
// Usuario 2 recibió la notificación: ¡Nueva noticia disponible!

newsPublisher.unsubscribe(subscriber1);
newsPublisher.notify("Otra noticia");
// Usuario 2 recibió la notificación: Otra noticia

Cuándo usarlo:

  • Sistemas de eventos
  • Actualizaciones de UI reactivas
  • Notificaciones push
  • Cambios de estado en aplicaciones

7. Patrón Mediator/Middleware

El patrón Mediator centraliza la comunicación entre objetos, evitando que se referencien directamente entre sí. Esto reduce el acoplamiento y facilita el mantenimiento.

Ejemplo sencillo:

class ChatRoom {
  constructor() {
    this.users = {};
  }
  
  register(user) {
    this.users[user.name] = user;
    user.chatRoom = this;
  }
  
  send(message, from, to) {
    if (to) {
      // Mensaje privado
      to.receive(message, from);
    } else {
      // Mensaje público
      Object.keys(this.users).forEach(key => {
        if (this.users[key] !== from) {
          this.users[key].receive(message, from);
        }
      });
    }
  }
}

class User {
  constructor(name) {
    this.name = name;
    this.chatRoom = null;
  }
  
  send(message, to) {
    this.chatRoom.send(message, this, to);
  }
  
  receive(message, from) {
    console.log(`${from.name} a ${this.name}: ${message}`);
  }
}

// Uso
const chatRoom = new ChatRoom();

const john = new User("John");
const jane = new User("Jane");
const joe = new User("Joe");

chatRoom.register(john);
chatRoom.register(jane);
chatRoom.register(joe);

john.send("Hola a todos"); 
// John a Jane: Hola a todos
// John a Joe: Hola a todos

jane.send("Hola John", john);
// Jane a John: Hola John

Cuándo usarlo:

  • Sistemas de chat o mensajería
  • Coordinación entre componentes complejos
  • Pipelines de procesamiento (middleware)

8. Patrón Strategy

El patrón Strategy define una familia de algoritmos, encapsula cada uno y los hace intercambiables. Permite que el algoritmo varíe independientemente de los clientes que lo usan.

Ejemplo sencillo:

// Estrategias de pago
class CreditCardPayment {
  pay(amount) {
    console.log(`Pagando $${amount} con tarjeta de crédito`);
  }
}

class PayPalPayment {
  pay(amount) {
    console.log(`Pagando $${amount} con PayPal`);
  }
}

class CryptoPayment {
  pay(amount) {
    console.log(`Pagando $${amount} con criptomonedas`);
  }
}

// Contexto
class ShoppingCart {
  constructor() {
    this.amount = 0;
    this.paymentStrategy = null;
  }
  
  setAmount(amount) {
    this.amount = amount;
  }
  
  setPaymentStrategy(strategy) {
    this.paymentStrategy = strategy;
  }
  
  checkout() {
    if (!this.paymentStrategy) {
      console.log("Por favor selecciona un método de pago");
      return;
    }
    this.paymentStrategy.pay(this.amount);
  }
}

// Uso
const cart = new ShoppingCart();
cart.setAmount(100);

cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(); // Pagando $100 con tarjeta de crédito

cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(); // Pagando $100 con PayPal

Cuándo usarlo:

  • Diferentes algoritmos para el mismo problema
  • Múltiples formas de procesar datos
  • Validaciones o cálculos variables

Patrones de Optimización y Performance

9. Patrón Flyweight

El patrón Flyweight minimiza el uso de memoria compartiendo la mayor cantidad posible de datos con objetos similares. Es útil cuando trabajas con grandes cantidades de objetos similares.

Ejemplo sencillo:

class Book {
  constructor(title, author, isbn) {
    this.title = title;
    this.author = author;
    this.isbn = isbn;
  }
}

// Flyweight Factory
class BookFactory {
  constructor() {
    this.existingBooks = {};
  }
  
  createBook(title, author, isbn) {
    const key = `${title}-${author}-${isbn}`;
    
    if (this.existingBooks[key]) {
      console.log("Reutilizando libro existente");
      return this.existingBooks[key];
    }
    
    console.log("Creando nuevo libro");
    const book = new Book(title, author, isbn);
    this.existingBooks[key] = book;
    return book;
  }
  
  getBookCount() {
    return Object.keys(this.existingBooks).length;
  }
}

// Uso
const factory = new BookFactory();

const book1 = factory.createBook("1984", "George Orwell", "123");
const book2 = factory.createBook("1984", "George Orwell", "123");
const book3 = factory.createBook("Animal Farm", "George Orwell", "456");

console.log(book1 === book2); // true - misma instancia
console.log(book1 === book3); // false - instancia diferente
console.log(`Libros únicos en memoria: ${factory.getBookCount()}`); // 2

Cuándo usarlo:

  • Grandes cantidades de objetos similares
  • Optimización de memoria
  • Sistemas de caché
Diagrama comparando memoria usada con y sin Flyweight
Diagrama comparando memoria usada con y sin Flyweight

Análisis de ahorro de memoria:

  • Sin Flyweight: Cada instancia del libro ocupa memoria completa (5 objetos × 1KB = 5KB)
  • Con Flyweight: Una única instancia compartida más referencias ligeras (1KB + overhead mínimo de referencias)
  • Ahorro: ~80% de memoria en este ejemplo. El ahorro aumenta proporcionalmente con más instancias

10. Patrón Prototype

El patrón Prototype crea nuevos objetos clonando instancias existentes. Es útil cuando la creación de un objeto es costosa o compleja.

Ejemplo sencillo:

class Car {
  constructor(model, year, features) {
    this.model = model;
    this.year = year;
    this.features = features;
  }
  
  clone() {
    return new Car(
      this.model,
      this.year,
      [...this.features] // Copia profunda del array
    );
  }
  
  displayInfo() {
    console.log(`${this.year} ${this.model}`);
    console.log("Características:", this.features.join(", "));
  }
}

// Uso
const originalCar = new Car("Tesla Model 3", 2024, [
  "Autopilot",
  "Techo panorámico",
  "Sistema de sonido premium"
]);

const clonedCar = originalCar.clone();
clonedCar.year = 2025;
clonedCar.features.push("Carga inalámbrica");

originalCar.displayInfo();
// 2024 Tesla Model 3
// Características: Autopilot, Techo panorámico, Sistema de sonido premium

clonedCar.displayInfo();
// 2025 Tesla Model 3
// Características: Autopilot, Techo panorámico, Sistema de sonido premium, Carga inalámbrica

Cuándo usarlo:

  • Creación de objetos costosa
  • Necesitas múltiples variaciones de un objeto base
  • Sistema de configuraciones predefinidas

Patrones Modernos con ES6+

11. Dynamic Import Pattern

El patrón de importación dinámica permite cargar módulos bajo demanda, mejorando el rendimiento inicial de tu aplicación.

Ejemplo sencillo:

// utils.js
export const heavyCalculation = (num) => {
  console.log("Ejecutando cálculo pesado...");
  return num * num * num;
};

// main.js
async function loadAndCalculate(num) {
  // Solo carga el módulo cuando se necesita
  const { heavyCalculation } = await import('./utils.js');
  return heavyCalculation(num);
}

// Uso con evento
document.getElementById('calculateBtn').addEventListener('click', async () => {
  const result = await loadAndCalculate(5);
  console.log(`Resultado: ${result}`);
});

Cuándo usarlo:

  • Code splitting en aplicaciones grandes
  • Cargar funcionalidad solo cuando el usuario la necesita
  • Optimizar el tiempo de carga inicial

12. Import on Interaction Pattern

Carga componentes o funcionalidad solo cuando el usuario interactúa con elementos específicos de la UI.

Ejemplo sencillo:

// chatWidget.js
export class ChatWidget {
  constructor() {
    console.log("Widget de chat inicializado");
  }
  
  open() {
    console.log("Abriendo chat...");
  }
}

// main.js
const chatButton = document.getElementById('chatBtn');
let chatWidget = null;

chatButton.addEventListener('click', async () => {
  if (!chatWidget) {
    // Solo carga el widget cuando el usuario hace clic
    const { ChatWidget } = await import('./chatWidget.js');
    chatWidget = new ChatWidget();
  }
  chatWidget.open();
});

Cuándo usarlo:

  • Widgets o modales que no se usan de inmediato
  • Funcionalidades premium o avanzadas
  • Reducir el bundle inicial

Patrones Anti-Pattern: Lo que Debes Evitar

Conocer los patrones correctos es como saber qué alimentos son saludables, pero identificar los anti-patrones es como reconocer qué comidas te harán daño.

God Object (Objeto Dios)

Un objeto que conoce demasiado o hace demasiado. Viola el principio de responsabilidad única.

// ❌ MAL - God Object
class Application {
  constructor() {
    this.users = [];
    this.products = [];
    this.orders = [];
  }
  
  addUser(user) { /* ... */ }
  deleteUser(id) { /* ... */ }
  updateUser(id, data) { /* ... */ }
  
  addProduct(product) { /* ... */ }
  deleteProduct(id) { /* ... */ }
  
  createOrder(order) { /* ... */ }
  processPayment(orderId) { /* ... */ }
  sendEmail(to, message) { /* ... */ }
  // ... y muchas más responsabilidades
}

// ✅ BIEN - Separación de responsabilidades
class UserManager {
  constructor() {
    this.users = [];
  }
  
  addUser(user) { /* ... */ }
  deleteUser(id) { /* ... */ }
}

class ProductManager {
  constructor() {
    this.products = [];
  }
  
  addProduct(product) { /* ... */ }
}

class OrderManager {
  constructor() {
    this.orders = [];
  }
  
  createOrder(order) { /* ... */ }
}

Callback Hell

Anidación excesiva de callbacks que dificulta la lectura y mantenimiento del código.

// ❌ MAL - Callback Hell
getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      getMoreData(c, function(d) {
        console.log(d);
      });
    });
  });
});

// ✅ BIEN - Async/Await
async function fetchAllData() {
  const a = await getData();
  const b = await getMoreData(a);
  const c = await getMoreData(b);
  const d = await getMoreData(c);
  console.log(d);
}

Mejores Prácticas al Usar Patrones de Diseño

  • No fuerces un patrón: Úsalos solo cuando resuelvan un problema real
  • Mantén la simplicidad: Un código simple es mejor que uno «elegante» pero complejo
  • Considera el contexto: Lo que funciona en un proyecto puede no ser adecuado en otro
  • Documenta tu decisión: Explica por qué elegiste un patrón específico
  • Refactoriza cuando sea necesario: Los patrones pueden evolucionar con tu código

Sugerencia de imagen: Infografía con las mejores prácticas enumeradas

Recursos Adicionales

Para profundizar en patrones de diseño en JavaScript moderno, te recomiendo estos recursos:

Ejercicios Prácticos

Ahora que has aprendido los patrones de diseño, es momento de poner en práctica tus conocimientos. Aquí tienes ejercicios progresivos que te ayudarán a dominar estos conceptos.

Ejercicio 1: Implementa un Singleton para Configuración

Objetivo: Crear un sistema de configuración global que solo pueda tener una instancia.

Requisitos:

  • Crea una clase AppConfig que siga el patrón Singleton
  • Debe permitir establecer y obtener valores de configuración (theme, language, apiUrl)
  • Incluye un método reset() que restaure la configuración por defecto
  • Asegúrate de que múltiples instancias devuelvan el mismo objeto

Código de inicio:

class AppConfig {
  // TODO: Implementa el patrón Singleton aquí
  
  constructor() {
    // Valores por defecto
    this.config = {
      theme: 'light',
      language: 'es',
      apiUrl: '<https://api.example.com>'
    };
  }
  
  get(key) {
    // TODO: Devuelve el valor de configuración
  }
  
  set(key, value) {
    // TODO: Establece un valor de configuración
  }
  
  reset() {
    // TODO: Restaura valores por defecto
  }
}

// Pruebas
const config1 = new AppConfig();
const config2 = new AppConfig();

console.log(config1 === config2); // Debe ser true
config1.set('theme', 'dark');
console.log(config2.get('theme')); // Debe ser 'dark'

Ejercicio 2: Sistema de Notificaciones con Observer

Objetivo: Crear un sistema de notificaciones que notifique a múltiples suscriptores.

Requisitos:

  • Implementa una clase NotificationCenter que actúe como Subject
  • Crea una clase Subscriber que pueda recibir notificaciones de diferentes tipos
  • Permite suscribirse a tipos específicos de notificaciones (email, sms, push)
  • Los suscriptores deben poder desuscribirse

Código de inicio:

class NotificationCenter {
  constructor() {
    this.subscribers = {
      email: [],
      sms: [],
      push: []
    };
  }
  
  subscribe(type, subscriber) {
    // TODO: Agregar suscriptor
  }
  
  unsubscribe(type, subscriber) {
    // TODO: Remover suscriptor
  }
  
  notify(type, message) {
    // TODO: Notificar a todos los suscriptores del tipo especificado
  }
}

class Subscriber {
  constructor(name) {
    this.name = name;
  }
  
  update(type, message) {
    console.log(`${this.name} recibió ${type}: ${message}`);
  }
}

// Pruebas
const notificationCenter = new NotificationCenter();

const user1 = new Subscriber("Usuario 1");
const user2 = new Subscriber("Usuario 2");

notificationCenter.subscribe('email', user1);
notificationCenter.subscribe('sms', user1);
notificationCenter.subscribe('email', user2);

notificationCenter.notify('email', 'Nuevo mensaje en tu bandeja');
// Debe notificar a user1 y user2

notificationCenter.notify('sms', 'Código de verificación: 123456');
// Solo debe notificar a user1

Ejercicio 3: Factory para Crear Diferentes Tipos de Usuarios

Objetivo: Implementar un Factory que cree diferentes tipos de usuarios con permisos específicos.

Requisitos:

  • Crea clases para tres tipos de usuarios: AdminUser, ModeratorUser, RegularUser
  • Cada tipo debe tener diferentes permisos (canDelete, canEdit, canView)
  • Implementa un UserFactory que cree el tipo correcto según un parámetro
  • Incluye un método getPermissions() en cada clase de usuario

Código de inicio:

class AdminUser {
  constructor(name) {
    this.name = name;
    this.role = 'admin';
  }
  
  getPermissions() {
    // TODO: Devuelve permisos de administrador
  }
}

class ModeratorUser {
  // TODO: Implementar
}

class RegularUser {
  // TODO: Implementar
}

class UserFactory {
  static createUser(type, name) {
    // TODO: Crear y devolver el tipo correcto de usuario
  }
}

// Pruebas
const admin = UserFactory.createUser('admin', 'Alice');
const mod = UserFactory.createUser('moderator', 'Bob');
const user = UserFactory.createUser('regular', 'Charlie');

console.log(admin.getPermissions()); 
// { canDelete: true, canEdit: true, canView: true }

console.log(mod.getPermissions()); 
// { canDelete: false, canEdit: true, canView: true }

console.log(user.getPermissions()); 
// { canDelete: false, canEdit: false, canView: true }

Ejercicio 4: Carrito de Compras con Strategy

Objetivo: Implementar un carrito de compras que pueda calcular descuentos usando diferentes estrategias.

Requisitos:

  • Crea estrategias de descuento: NoDiscount, PercentageDiscount, FixedDiscount
  • Implementa una clase ShoppingCart que use estas estrategias
  • Permite cambiar la estrategia de descuento dinámicamente
  • Incluye un método calculateTotal() que aplique el descuento correspondiente

Código de inicio:

class NoDiscount {
  calculate(amount) {
    return amount;
  }
}

class PercentageDiscount {
  constructor(percentage) {
    this.percentage = percentage;
  }
  
  calculate(amount) {
    // TODO: Calcular descuento porcentual
  }
}

class FixedDiscount {
  // TODO: Implementar descuento fijo
}

class ShoppingCart {
  constructor() {
    this.items = [];
    this.discountStrategy = new NoDiscount();
  }
  
  addItem(item, price) {
    this.items.push({ item, price });
  }
  
  setDiscountStrategy(strategy) {
    // TODO: Establecer estrategia de descuento
  }
  
  calculateTotal() {
    // TODO: Calcular total con descuento aplicado
  }
}

// Pruebas
const cart = new ShoppingCart();
cart.addItem('Laptop', 1000);
cart.addItem('Mouse', 50);

console.log(cart.calculateTotal()); // 1050

cart.setDiscountStrategy(new PercentageDiscount(10));
console.log(cart.calculateTotal()); // 945 (10% de descuento)

cart.setDiscountStrategy(new FixedDiscount(100));
console.log(cart.calculateTotal()); // 950 (descuento fijo de 100)

Ejercicio 5: Proyecto Integrador – Sistema de Blog

Objetivo: Crear un sistema de blog completo que combine múltiples patrones de diseño.

Requisitos:

  • Usa Singleton para gestionar la configuración del blog
  • Implementa Factory para crear diferentes tipos de posts (Text, Image, Video)
  • Aplica Observer para notificar a los suscriptores cuando hay nuevos posts
  • Utiliza Proxy para controlar el acceso a posts premium
  • Implementa Strategy para diferentes algoritmos de ordenamiento (por fecha, por popularidad)

Funcionalidades requeridas:

  • Crear posts de diferentes tipos
  • Suscribirse a notificaciones de nuevos posts
  • Controlar acceso a contenido premium
  • Ordenar posts usando diferentes criterios

Este ejercicio es avanzado y te ayudará a entender cómo combinar patrones en una aplicación real.

Soluciones y Discusión

Las soluciones a estos ejercicios, junto con explicaciones detalladas, están disponibles en el repositorio de GitHub del blog. Te animo a intentar resolverlos por tu cuenta primero antes de consultar las soluciones.

Recuerda: No hay una única forma correcta de implementar estos patrones. Lo importante es entender el problema que resuelven y adaptar la solución a tu contexto específico.

Conclusión

Si hay algo que quiero que te lleves de este post: no se trata de memorizar código como si fueras una enciclopedia andante. Se trata de reconocer problemas familiares y saber qué herramienta sacar de tu caja. Ahora sabes cuándo aplicar un Singleton, un Observer o cualquier otro patrón.

¿Qué patrón de diseño te resultó más útil? ¿Ya has implementado alguno en tus proyectos? Comparte tu experiencia en los comentarios. Me encantaría conocer cómo estás aplicando estos patrones en tus aplicaciones.


Avatar de darkusphantom

Sigueme en mis redes sociales para más contenido


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *