HDP115

Patrones de Diseño en JavaScript

Aprende sobre los patrones de diseño más utilizados en JavaScript, cómo implementarlos y cuándo aplicarlos para resolver problemas comunes de desarrollo.

CE

Cristian Escalante

Última actualización: 24 de abril de 2025

javascript
patrones de diseño
programación avanzada
desarrollo web

Patrones de Diseño

¿Qué son los patrones de diseño?

Los patrones de diseño son soluciones probadas y documentadas para problemas comunes en el desarrollo de software. Representan las mejores prácticas utilizadas por desarrolladores experimentados y proporcionan un enfoque estandarizado para resolver ciertos desafíos.

En JavaScript, los patrones de diseño son especialmente útiles debido a la naturaleza flexible del lenguaje y su paradigma de programación basado en prototipos.

// Los patrones de diseño no son fragmentos de código para copiar y pegar
// Son conceptos y soluciones que adaptamos a nuestros problemas específicos

Beneficios de utilizar patrones de diseño

  • Código más mantenible: Estructura organizada y predecible
  • Comunicación mejorada: Vocabulario común entre desarrolladores
  • Soluciones probadas: Evita reinventar la rueda
  • Escalabilidad: Facilita el crecimiento de aplicaciones
  • Reutilización: Promueve prácticas de código reutilizable

Categorías de patrones de diseño

Patrones creacionales

Los patrones creacionales se enfocan en mecanismos de creación de objetos, tratando de crear objetos de manera adecuada para cada situación.

Patrón Factory (Fábrica)

El patrón Factory proporciona una interfaz para crear objetos en una superclase, pero permite que las subclases alteren el tipo de objetos que se crearán.

// Ejemplo de Factory Pattern
class VehicleFactory {
  createVehicle(type) {
    switch(type) {
      case 'car':
        return new Car();
      case 'truck':
        return new Truck();
      case 'motorcycle':
        return new Motorcycle();
      default:
        throw new Error('Vehicle type not supported');
    }
  }
}

// Uso
const factory = new VehicleFactory();
const myCar = factory.createVehicle('car');

Patrón Singleton

El patrón Singleton garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella.

// Ejemplo de Singleton Pattern
class DatabaseConnection {
  constructor() {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }
    
    this.connection = 'Connected to database';
    DatabaseConnection.instance = this;
  }
  
  query(sql) {
    console.log(`Executing: ${sql}`);
    return `Results for ${sql}`;
  }
}

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

console.log(db1 === db2); // true - ambas variables referencian la misma instancia

Patrones estructurales

Los patrones estructurales se ocupan de la composición de clases y objetos para formar estructuras más grandes y complejas.

Patrón Module (Módulo)

El patrón Module encapsula funcionalidades, ocultando detalles de implementación y exponiendo una API pública.

// Ejemplo de Module Pattern
const shoppingCart = (function() {
  // Variables privadas
  let items = [];
  
  // Métodos privados
  function calculateTotal() {
    return items.reduce((total, item) => total + item.price, 0);
  }
  
  // API pública
  return {
    addItem: function(item) {
      items.push(item);
    },
    removeItem: function(index) {
      items.splice(index, 1);
    },
    getItems: function() {
      return [...items]; // Devuelve una copia para evitar modificaciones directas
    },
    getTotal: function() {
      return calculateTotal();
    }
  };
})();

// Uso
shoppingCart.addItem({ name: 'Laptop', price: 999 });
console.log(shoppingCart.getTotal()); // 999

Patrón Decorator (Decorador)

El patrón Decorator permite añadir nuevas funcionalidades a objetos existentes sin alterar su estructura.

// Ejemplo de Decorator Pattern
// Componente base
class Coffee {
  getCost() {
    return 5;
  }
  
  getDescription() {
    return 'Simple coffee';
  }
}

// Decorador
class MilkDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }
  
  getCost() {
    return this.coffee.getCost() + 1;
  }
  
  getDescription() {
    return `${this.coffee.getDescription()}, with milk`;
  }
}

// Uso
let myCoffee = new Coffee();
myCoffee = new MilkDecorator(myCoffee);
console.log(myCoffee.getDescription()); // "Simple coffee, with milk"
console.log(myCoffee.getCost()); // 6

Patrones de comportamiento

Los patrones de comportamiento se encargan de la comunicación efectiva y la asignación de responsabilidades entre objetos.

Patrón Observer (Observador)

El patrón Observer define una dependencia uno-a-muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados y actualizados automáticamente.

// Ejemplo de Observer Pattern
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} received: ${data}`);
  }
}

// Uso
const subject = new Subject();
const observer1 = new Observer('Observer 1');
const observer2 = new Observer('Observer 2');

subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify('Hello observers!');
// Observer 1 received: Hello observers!
// Observer 2 received: Hello observers!

Patrón Iterator (Iterador)

El patrón Iterator proporciona una forma de acceder secuencialmente a los elementos de una colección sin exponer su representación subyacente.

// Ejemplo de Iterator Pattern
class IterableCollection {
  constructor(items) {
    this.items = items;
  }
  
  createIterator() {
    return new Iterator(this);
  }
}

class Iterator {
  constructor(collection) {
    this.collection = collection;
    this.index = 0;
  }
  
  hasNext() {
    return this.index < this.collection.items.length;
  }
  
  next() {
    return this.hasNext() ? this.collection.items[this.index++] : null;
  }
}

// Uso
const collection = new IterableCollection(['a', 'b', 'c']);
const iterator = collection.createIterator();

while (iterator.hasNext()) {
  console.log(iterator.next());
}
// a
// b
// c

Patrones de arquitectura

Los patrones de arquitectura son esquemas para la organización estructural de sistemas de software.

Patrón MVC (Modelo-Vista-Controlador)

MVC separa la lógica de la aplicación en tres componentes interconectados: el Modelo (datos), la Vista (interfaz de usuario) y el Controlador (lógica de control).

// Ejemplo simplificado de MVC
// Modelo
class TodoModel {
  constructor() {
    this.todos = [];
    this.onChangeCb = null;
  }
  
  addTodo(todoText) {
    this.todos.push({
      id: this.todos.length + 1,
      text: todoText,
      completed: false
    });
    this._commit();
  }
  
  toggleTodo(id) {
    this.todos = this.todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
    this._commit();
  }
  
  bindTodoListChanged(callback) {
    this.onChangeCb = callback;
  }
  
  _commit() {
    if (this.onChangeCb) {
      this.onChangeCb(this.todos);
    }
  }
}

// Vista
class TodoView {
  constructor() {
    this.app = document.getElementById('app');
    this.todoList = document.createElement('ul');
    this.app.appendChild(this.todoList);
  }
  
  displayTodos(todos) {
    this.todoList.innerHTML = '';
    
    todos.forEach(todo => {
      const li = document.createElement('li');
      li.textContent = todo.text;
      li.dataset.id = todo.id;
      if (todo.completed) {
        li.style.textDecoration = 'line-through';
      }
      this.todoList.appendChild(li);
    });
  }
  
  bindAddTodo(handler) {
    // Implementación de enlace de eventos
  }
  
  bindToggleTodo(handler) {
    this.todoList.addEventListener('click', event => {
      if (event.target.tagName.toLowerCase() === 'li') {
        handler(parseInt(event.target.dataset.id));
      }
    });
  }
}

// Controlador
class TodoController {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    
    this.model.bindTodoListChanged(this.onTodoListChanged);
    this.view.bindAddTodo(this.handleAddTodo);
    this.view.bindToggleTodo(this.handleToggleTodo);
    
    this.onTodoListChanged(this.model.todos);
  }
  
  onTodoListChanged = todos => {
    this.view.displayTodos(todos);
  }
  
  handleAddTodo = todoText => {
    this.model.addTodo(todoText);
  }
  
  handleToggleTodo = id => {
    this.model.toggleTodo(id);
  }
}

// Uso
const app = new TodoController(new TodoModel(), new TodoView());

Patrón MVVM (Modelo-Vista-ViewModel)

MVVM facilita la separación del desarrollo de la interfaz gráfica de la lógica de negocio o la lógica de backend.

// MVVM se implementa típicamente con frameworks como Vue.js
// Ejemplo conceptual simplificado
class Model {
  constructor() {
    this.data = { counter: 0 };
  }
  
  incrementCounter() {
    this.data.counter++;
    return this.data.counter;
  }
}

class ViewModel {
  constructor(model) {
    this.model = model;
    this.observers = {};
  }
  
  get counter() {
    return this.model.data.counter;
  }
  
  incrementCounter() {
    const newValue = this.model.incrementCounter();
    this.notify('counter', newValue);
  }
  
  observe(property, callback) {
    if (!this.observers[property]) {
      this.observers[property] = [];
    }
    this.observers[property].push(callback);
  }
  
  notify(property, value) {
    if (this.observers[property]) {
      this.observers[property].forEach(callback => callback(value));
    }
  }
}

// En una aplicación real, la Vista sería el DOM o componentes de UI

Patrón de módulo revelador

El patrón de módulo revelador es una variante del patrón módulo que proporciona una mejor organización y legibilidad.

// Ejemplo de Revealing Module Pattern
const calculator = (function() {
  // Variables y funciones privadas
  let result = 0;
  
  function add(a, b) {
    return a + b;
  }
  
  function subtract(a, b) {
    return a - b;
  }
  
  function multiply(a, b) {
    return a * b;
  }
  
  function divide(a, b) {
    if (b === 0) throw new Error("Cannot divide by zero");
    return a / b;
  }
  
  // Revela solo lo que queremos hacer público
  return {
    add: add,
    subtract: subtract,
    multiply: multiply,
    divide: divide
  };
})();

// Uso
console.log(calculator.add(5, 3)); // 8

Patrón publicador/suscriptor (Pub/Sub)

El patrón Pub/Sub permite la comunicación entre objetos sin que estén directamente acoplados.

// Ejemplo de Pub/Sub Pattern
class PubSub {
  constructor() {
    this.subscribers = {};
  }
  
  subscribe(event, callback) {
    if (!this.subscribers[event]) {
      this.subscribers[event] = [];
    }
    this.subscribers[event].push(callback);
    
    // Devuelve una función para cancelar la suscripción
    return () => {
      this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback);
    };
  }
  
  publish(event, data) {
    if (!this.subscribers[event]) {
      return;
    }
    
    this.subscribers[event].forEach(callback => {
      callback(data);
    });
  }
}

// Uso
const pubSub = new PubSub();

const unsubscribe = pubSub.subscribe('userLoggedIn', user => {
  console.log(`Welcome ${user.name}!`);
});

pubSub.publish('userLoggedIn', { name: 'John' }); // "Welcome John!"

// Cancelar suscripción
unsubscribe();

Composición frente a herencia

La composición es un enfoque alternativo a la herencia que favorece la flexibilidad y la reutilización de código.

// Enfoque de herencia
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  eat() {
    console.log(`${this.name} is eating.`);
  }
}

class Bird extends Animal {
  fly() {
    console.log(`${this.name} is flying.`);
  }
}

// Enfoque de composición
function createAnimal(name) {
  return {
    name,
    eat() {
      console.log(`${name} is eating.`);
    }
  };
}

function createFlyingAnimal(name) {
  return {
    ...createAnimal(name),
    fly() {
      console.log(`${name} is flying.`);
    }
  };
}

// Uso de composición
const sparrow = createFlyingAnimal('Sparrow');
sparrow.eat(); // "Sparrow is eating."
sparrow.fly(); // "Sparrow is flying."

Ventajas de la composición

  • Flexibilidad: Más fácil de modificar y extender
  • Evita problemas de la herencia: Como el problema del diamante
  • Favorece la reutilización: Componentes pequeños y específicos
  • Menor acoplamiento: Objetos menos dependientes entre sí

Implementación práctica de patrones

Caso de estudio: Sistema de carrito de compras

Veamos cómo podemos aplicar varios patrones en un sistema de carrito de compras:

// Patrón Singleton para el carrito
const ShoppingCart = (function() {
  let instance;
  
  function createInstance() {
    // Patrón Module para encapsular la lógica del carrito
    const items = [];
    
    // Métodos privados
    function calculateTotal() {
      return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    }
    
    // Patrón Observer para notificaciones
    const observers = [];
    
    function notifyObservers() {
      observers.forEach(observer => observer.update(items));
    }
    
    // API pública
    return {
      addItem(item) {
        const existingItem = items.find(i => i.id === item.id);
        
        if (existingItem) {
          existingItem.quantity += item.quantity || 1;
        } else {
          items.push({...item, quantity: item.quantity || 1});
        }
        
        notifyObservers();
      },
      
      removeItem(id) {
        const index = items.findIndex(item => item.id === id);
        if (index !== -1) {
          items.splice(index, 1);
          notifyObservers();
        }
      },
      
      getItems() {
        return [...items];
      },
      
      getTotal() {
        return calculateTotal();
      },
      
      subscribe(observer) {
        observers.push(observer);
      },
      
      unsubscribe(observer) {
        const index = observers.indexOf(observer);
        if (index !== -1) {
          observers.splice(index, 1);
        }
      }
    };
  }
  
  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

// Uso
const cart = ShoppingCart.getInstance();

// Añadir un observador (patrón Observer)
const cartTotalElement = {
  update(items) {
    const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    console.log(`Cart total: $${total.toFixed(2)}`);
  }
};

cart.subscribe(cartTotalElement);

// Añadir productos
cart.addItem({ id: 1, name: "Laptop", price: 999.99 });
// Cart total: $999.99

cart.addItem({ id: 2, name: "Mouse", price: 29.99 });
// Cart total: $1029.98

Cuándo usar cada patrón

PatrónCuándo usarlo
FactoryCuando la creación de objetos involucra lógica compleja o cuando necesitas crear diferentes tipos de objetos basados en condiciones.
SingletonPara recursos compartidos como conexiones a bases de datos, configuraciones o cachés.
ModulePara organizar y encapsular código relacionado, ocultando detalles de implementación.
ObserverCuando tienes objetos que necesitan ser notificados de cambios en otros objetos.
MVC/MVVMPara aplicaciones con interfaces de usuario complejas que requieren separación de responsabilidades.
Pub/SubPara comunicación desacoplada entre componentes que no necesitan conocerse entre sí.

Mejores prácticas

  1. No sobreutilizar patrones: Utilízalos solo cuando resuelvan un problema específico.
  2. Mantener la simplicidad: El código simple y legible es a menudo mejor que el código complejo con patrones.
  3. Documentar el uso de patrones: Ayuda a otros desarrolladores a entender tu código.
  4. Adaptar los patrones: No sigas rígidamente las implementaciones clásicas; adapta los patrones a tus necesidades.
  5. Combinar patrones: A menudo, los problemas complejos requieren combinar varios patrones.

Conclusión

Los patrones de diseño son herramientas poderosas en el arsenal de cualquier desarrollador JavaScript. Proporcionan soluciones probadas a problemas comunes y ayudan a estructurar el código de manera más mantenible y escalable. Sin embargo, es importante recordar que los patrones son guías, no reglas estrictas, y deben aplicarse con criterio según las necesidades específicas de cada proyecto.

Storage y Cookies
Aprende a persistir datos en el navegador utilizando diferen...
Testing en JavaScript
Aprende a implementar pruebas efectivas para garantizar la c...
Referencias
Addy Osmani. Learning JavaScript Design Patterns. https://addyosmani.com/resources/essentialjsdesignpatterns/book/
Dofactory. Design Patterns in JavaScript. https://www.dofactory.com/javascript/design-patterns

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025