JavaScript Patterns

JavaScript Design Patterns: Essential Patterns for Better Code

Master JavaScript design patterns including Singleton, Observer, Factory, Module, Strategy, and more. Learn when and how to apply these patterns effectively.

By JavaScript Document Team
design-patternsarchitecturesingletonobserverfactorymodule

Design patterns are reusable solutions to common programming problems. They provide proven approaches to structuring code, improving maintainability, and solving recurring design challenges. This guide covers essential JavaScript design patterns with practical examples.

Creational Patterns

Creational patterns deal with object creation mechanisms, providing flexibility in deciding which objects to create and how to create them.

Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides global access to it.

// Classic Singleton Pattern
class Singleton {
  constructor() {
    if (Singleton.instance) {
      return Singleton.instance;
    }

    this.data = {};
    this.timestamp = Date.now();
    Singleton.instance = this;

    return this;
  }

  getInstance() {
    return this;
  }

  setData(key, value) {
    this.data[key] = value;
  }

  getData(key) {
    return this.data[key];
  }

  clearData() {
    this.data = {};
  }
}

// Modern Singleton using Module Pattern
const ConfigManager = (() => {
  let instance;

  function createInstance() {
    return {
      config: {},

      set(key, value) {
        this.config[key] = value;
      },

      get(key) {
        return this.config[key];
      },

      getAll() {
        return { ...this.config };
      },

      reset() {
        this.config = {};
      },
    };
  }

  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

// Lazy Singleton with Proxy
class LazyLoggerSingleton {
  constructor() {
    this.logs = [];
    this.level = 'info';
  }

  log(message, level = 'info') {
    const timestamp = new Date().toISOString();
    this.logs.push({ message, level, timestamp });

    if (this.shouldOutput(level)) {
      console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`);
    }
  }

  shouldOutput(level) {
    const levels = { debug: 0, info: 1, warn: 2, error: 3 };
    return levels[level] >= levels[this.level];
  }

  setLevel(level) {
    this.level = level;
  }

  getLogs(level) {
    return level ? this.logs.filter((log) => log.level === level) : this.logs;
  }

  clear() {
    this.logs = [];
  }
}

const Logger = new Proxy(LazyLoggerSingleton, {
  construct(target, args) {
    if (!target.instance) {
      target.instance = new target(...args);
    }
    return target.instance;
  },
});

// Database Connection Singleton
class DatabaseConnection {
  constructor(config) {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }

    this.config = config;
    this.connected = false;
    this.queries = [];

    DatabaseConnection.instance = this;
    return this;
  }

  async connect() {
    if (this.connected) {
      return Promise.resolve();
    }

    // Simulate database connection
    return new Promise((resolve) => {
      setTimeout(() => {
        this.connected = true;
        console.log('Database connected');
        resolve();
      }, 1000);
    });
  }

  async query(sql, params = []) {
    if (!this.connected) {
      await this.connect();
    }

    const query = { sql, params, timestamp: Date.now() };
    this.queries.push(query);

    // Simulate query execution
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ data: [], query });
      }, 100);
    });
  }

  getQueryHistory() {
    return [...this.queries];
  }

  disconnect() {
    this.connected = false;
    console.log('Database disconnected');
  }
}

// Usage examples
const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();
console.log(config1 === config2); // true

config1.set('apiUrl', 'https://api.example.com');
console.log(config2.get('apiUrl')); // 'https://api.example.com'

const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true

logger1.log('Application started');
logger2.log('User logged in');
console.log(logger1.getLogs()); // Both logs appear

Factory Pattern

The Factory pattern creates objects without specifying their exact classes, providing an interface for creating families of related objects.

// Simple Factory Pattern
class VehicleFactory {
  static createVehicle(type, options = {}) {
    switch (type.toLowerCase()) {
      case 'car':
        return new Car(options);
      case 'motorcycle':
        return new Motorcycle(options);
      case 'truck':
        return new Truck(options);
      default:
        throw new Error(`Vehicle type "${type}" is not supported`);
    }
  }

  static getSupportedTypes() {
    return ['car', 'motorcycle', 'truck'];
  }
}

class Vehicle {
  constructor(options = {}) {
    this.make = options.make || 'Unknown';
    this.model = options.model || 'Unknown';
    this.year = options.year || new Date().getFullYear();
    this.color = options.color || 'white';
  }

  start() {
    console.log(`${this.make} ${this.model} started`);
  }

  stop() {
    console.log(`${this.make} ${this.model} stopped`);
  }

  getInfo() {
    return `${this.year} ${this.make} ${this.model} (${this.color})`;
  }
}

class Car extends Vehicle {
  constructor(options = {}) {
    super(options);
    this.type = 'car';
    this.doors = options.doors || 4;
    this.transmission = options.transmission || 'automatic';
  }

  honk() {
    console.log('Beep beep!');
  }
}

class Motorcycle extends Vehicle {
  constructor(options = {}) {
    super(options);
    this.type = 'motorcycle';
    this.engineSize = options.engineSize || 250;
    this.hasHelmet = options.hasHelmet || false;
  }

  revEngine() {
    console.log('Vroom vroom!');
  }
}

class Truck extends Vehicle {
  constructor(options = {}) {
    super(options);
    this.type = 'truck';
    this.capacity = options.capacity || 1000;
    this.axles = options.axles || 2;
  }

  loadCargo(weight) {
    if (weight <= this.capacity) {
      console.log(`Loaded ${weight}kg cargo`);
      return true;
    } else {
      console.log(
        `Cannot load ${weight}kg - exceeds capacity of ${this.capacity}kg`
      );
      return false;
    }
  }
}

// Abstract Factory Pattern
class UIComponentFactory {
  static createFactory(theme) {
    switch (theme) {
      case 'light':
        return new LightThemeFactory();
      case 'dark':
        return new DarkThemeFactory();
      case 'material':
        return new MaterialThemeFactory();
      default:
        throw new Error(`Theme "${theme}" is not supported`);
    }
  }
}

class ThemeFactory {
  createButton(text, onClick) {
    throw new Error('createButton method must be implemented');
  }

  createInput(placeholder) {
    throw new Error('createInput method must be implemented');
  }

  createModal(title, content) {
    throw new Error('createModal method must be implemented');
  }
}

class LightThemeFactory extends ThemeFactory {
  createButton(text, onClick) {
    return {
      type: 'button',
      text,
      onClick,
      styles: {
        backgroundColor: '#f0f0f0',
        color: '#333',
        border: '1px solid #ccc',
        borderRadius: '4px',
        padding: '8px 16px',
      },
      render() {
        const button = document.createElement('button');
        button.textContent = this.text;
        button.onclick = this.onClick;
        Object.assign(button.style, this.styles);
        return button;
      },
    };
  }

  createInput(placeholder) {
    return {
      type: 'input',
      placeholder,
      styles: {
        backgroundColor: '#fff',
        color: '#333',
        border: '1px solid #ccc',
        borderRadius: '4px',
        padding: '8px',
      },
      render() {
        const input = document.createElement('input');
        input.placeholder = this.placeholder;
        Object.assign(input.style, this.styles);
        return input;
      },
    };
  }
}

class DarkThemeFactory extends ThemeFactory {
  createButton(text, onClick) {
    return {
      type: 'button',
      text,
      onClick,
      styles: {
        backgroundColor: '#333',
        color: '#fff',
        border: '1px solid #555',
        borderRadius: '4px',
        padding: '8px 16px',
      },
      render() {
        const button = document.createElement('button');
        button.textContent = this.text;
        button.onclick = this.onClick;
        Object.assign(button.style, this.styles);
        return button;
      },
    };
  }

  createInput(placeholder) {
    return {
      type: 'input',
      placeholder,
      styles: {
        backgroundColor: '#222',
        color: '#fff',
        border: '1px solid #555',
        borderRadius: '4px',
        padding: '8px',
      },
      render() {
        const input = document.createElement('input');
        input.placeholder = this.placeholder;
        Object.assign(input.style, this.styles);
        return input;
      },
    };
  }
}

// Plugin Factory Pattern
class PluginFactory {
  constructor() {
    this.plugins = new Map();
  }

  register(name, pluginClass) {
    this.plugins.set(name, pluginClass);
  }

  create(name, options = {}) {
    const PluginClass = this.plugins.get(name);

    if (!PluginClass) {
      throw new Error(`Plugin "${name}" is not registered`);
    }

    return new PluginClass(options);
  }

  getAvailablePlugins() {
    return Array.from(this.plugins.keys());
  }

  unregister(name) {
    return this.plugins.delete(name);
  }
}

// Plugin base class
class Plugin {
  constructor(options = {}) {
    this.name = options.name || 'Unknown Plugin';
    this.version = options.version || '1.0.0';
    this.enabled = true;
  }

  initialize() {
    console.log(`${this.name} v${this.version} initialized`);
  }

  execute() {
    throw new Error('execute method must be implemented');
  }

  destroy() {
    console.log(`${this.name} destroyed`);
  }
}

class LoggerPlugin extends Plugin {
  constructor(options = {}) {
    super({ name: 'Logger Plugin', ...options });
    this.level = options.level || 'info';
  }

  execute(message, level = this.level) {
    const timestamp = new Date().toISOString();
    console.log(`[${timestamp}] ${level.toUpperCase()}: ${message}`);
  }
}

class ValidatorPlugin extends Plugin {
  constructor(options = {}) {
    super({ name: 'Validator Plugin', ...options });
    this.rules = options.rules || {};
  }

  execute(data) {
    const errors = [];

    for (const [field, value] of Object.entries(data)) {
      const rule = this.rules[field];
      if (rule && !rule.validate(value)) {
        errors.push({
          field,
          message: rule.message || `${field} is invalid`,
        });
      }
    }

    return {
      isValid: errors.length === 0,
      errors,
    };
  }
}

// Usage examples
const car = VehicleFactory.createVehicle('car', {
  make: 'Toyota',
  model: 'Camry',
  year: 2023,
  doors: 4,
});

const motorcycle = VehicleFactory.createVehicle('motorcycle', {
  make: 'Honda',
  model: 'CBR600RR',
  engineSize: 600,
});

console.log(car.getInfo()); // "2023 Toyota Camry (white)"
car.honk(); // "Beep beep!"

const lightFactory = UIComponentFactory.createFactory('light');
const darkFactory = UIComponentFactory.createFactory('dark');

const lightButton = lightFactory.createButton('Click me', () =>
  alert('Light theme!')
);
const darkButton = darkFactory.createButton('Click me', () =>
  alert('Dark theme!')
);

// Plugin factory usage
const pluginFactory = new PluginFactory();
pluginFactory.register('logger', LoggerPlugin);
pluginFactory.register('validator', ValidatorPlugin);

const logger = pluginFactory.create('logger', { level: 'debug' });
const validator = pluginFactory.create('validator', {
  rules: {
    email: {
      validate: (value) => /\S+@\S+\.\S+/.test(value),
      message: 'Invalid email format',
    },
  },
});

logger.execute('Application started', 'info');
const result = validator.execute({ email: 'invalid-email' });
console.log(result); // { isValid: false, errors: [...] }

Builder Pattern

The Builder pattern constructs complex objects step by step, allowing you to create different representations of an object using the same construction process.

// Fluent Builder Pattern
class QueryBuilder {
  constructor() {
    this.reset();
  }

  reset() {
    this.query = {
      type: '',
      table: '',
      fields: [],
      conditions: [],
      joins: [],
      orderBy: [],
      groupBy: [],
      limit: null,
      offset: null,
    };
    return this;
  }

  select(fields = ['*']) {
    this.query.type = 'SELECT';
    this.query.fields = Array.isArray(fields) ? fields : [fields];
    return this;
  }

  from(table) {
    this.query.table = table;
    return this;
  }

  where(condition, operator = 'AND') {
    this.query.conditions.push({ condition, operator });
    return this;
  }

  orWhere(condition) {
    return this.where(condition, 'OR');
  }

  join(table, on, type = 'INNER') {
    this.query.joins.push({ table, on, type });
    return this;
  }

  leftJoin(table, on) {
    return this.join(table, on, 'LEFT');
  }

  rightJoin(table, on) {
    return this.join(table, on, 'RIGHT');
  }

  orderBy(field, direction = 'ASC') {
    this.query.orderBy.push({ field, direction });
    return this;
  }

  groupBy(fields) {
    this.query.groupBy = Array.isArray(fields) ? fields : [fields];
    return this;
  }

  limit(count) {
    this.query.limit = count;
    return this;
  }

  offset(count) {
    this.query.offset = count;
    return this;
  }

  build() {
    let sql = '';

    if (this.query.type === 'SELECT') {
      sql += `SELECT ${this.query.fields.join(', ')}`;
      sql += ` FROM ${this.query.table}`;

      // Add joins
      this.query.joins.forEach((join) => {
        sql += ` ${join.type} JOIN ${join.table} ON ${join.on}`;
      });

      // Add conditions
      if (this.query.conditions.length > 0) {
        sql += ' WHERE ';
        sql += this.query.conditions
          .map((cond, index) => {
            if (index === 0) return cond.condition;
            return `${cond.operator} ${cond.condition}`;
          })
          .join(' ');
      }

      // Add group by
      if (this.query.groupBy.length > 0) {
        sql += ` GROUP BY ${this.query.groupBy.join(', ')}`;
      }

      // Add order by
      if (this.query.orderBy.length > 0) {
        sql += ' ORDER BY ';
        sql += this.query.orderBy
          .map((order) => `${order.field} ${order.direction}`)
          .join(', ');
      }

      // Add limit and offset
      if (this.query.limit !== null) {
        sql += ` LIMIT ${this.query.limit}`;
      }

      if (this.query.offset !== null) {
        sql += ` OFFSET ${this.query.offset}`;
      }
    }

    return sql;
  }

  execute() {
    const sql = this.build();
    console.log('Executing SQL:', sql);
    // Here you would execute the actual SQL query
    return Promise.resolve({ sql, data: [] });
  }

  clone() {
    const builder = new QueryBuilder();
    builder.query = JSON.parse(JSON.stringify(this.query));
    return builder;
  }
}

// Form Builder Pattern
class FormBuilder {
  constructor() {
    this.form = {
      fields: [],
      validators: [],
      layout: 'vertical',
      theme: 'default',
    };
  }

  addField(config) {
    const field = {
      type: config.type || 'text',
      name: config.name,
      label: config.label || config.name,
      placeholder: config.placeholder || '',
      required: config.required || false,
      value: config.value || '',
      attributes: config.attributes || {},
      validators: config.validators || [],
    };

    this.form.fields.push(field);
    return this;
  }

  addTextField(name, label, options = {}) {
    return this.addField({
      type: 'text',
      name,
      label,
      ...options,
    });
  }

  addEmailField(name, label, options = {}) {
    return this.addField({
      type: 'email',
      name,
      label,
      validators: [
        {
          type: 'email',
          message: 'Please enter a valid email address',
        },
      ],
      ...options,
    });
  }

  addPasswordField(name, label, options = {}) {
    return this.addField({
      type: 'password',
      name,
      label,
      validators: [
        {
          type: 'minLength',
          value: 8,
          message: 'Password must be at least 8 characters long',
        },
      ],
      ...options,
    });
  }

  addSelectField(name, label, options, config = {}) {
    return this.addField({
      type: 'select',
      name,
      label,
      options,
      ...config,
    });
  }

  addCheckboxField(name, label, options = {}) {
    return this.addField({
      type: 'checkbox',
      name,
      label,
      ...options,
    });
  }

  addValidator(validator) {
    this.form.validators.push(validator);
    return this;
  }

  setLayout(layout) {
    this.form.layout = layout;
    return this;
  }

  setTheme(theme) {
    this.form.theme = theme;
    return this;
  }

  build() {
    const formElement = document.createElement('form');
    formElement.className = `form form--${this.form.layout} form--${this.form.theme}`;

    this.form.fields.forEach((field) => {
      const fieldElement = this.createFieldElement(field);
      formElement.appendChild(fieldElement);
    });

    return {
      element: formElement,
      config: this.form,
      validate: () => this.validate(),
      getData: () => this.getData(formElement),
      setData: (data) => this.setData(formElement, data),
    };
  }

  createFieldElement(field) {
    const wrapper = document.createElement('div');
    wrapper.className = `form-field form-field--${field.type}`;

    // Create label
    if (field.label) {
      const label = document.createElement('label');
      label.textContent = field.label;
      label.setAttribute('for', field.name);
      if (field.required) {
        label.textContent += ' *';
      }
      wrapper.appendChild(label);
    }

    // Create input
    let input;

    switch (field.type) {
      case 'select':
        input = document.createElement('select');
        if (field.options) {
          field.options.forEach((option) => {
            const optionElement = document.createElement('option');
            optionElement.value = option.value;
            optionElement.textContent = option.label;
            input.appendChild(optionElement);
          });
        }
        break;

      case 'textarea':
        input = document.createElement('textarea');
        break;

      default:
        input = document.createElement('input');
        input.type = field.type;
    }

    input.name = field.name;
    input.id = field.name;
    input.value = field.value;
    input.placeholder = field.placeholder;
    input.required = field.required;

    // Apply additional attributes
    Object.entries(field.attributes).forEach(([key, value]) => {
      input.setAttribute(key, value);
    });

    wrapper.appendChild(input);

    return wrapper;
  }

  validate() {
    // Implementation for form validation
    return { isValid: true, errors: [] };
  }

  getData(formElement) {
    const data = {};
    const inputs = formElement.querySelectorAll('input, select, textarea');

    inputs.forEach((input) => {
      if (input.type === 'checkbox') {
        data[input.name] = input.checked;
      } else {
        data[input.name] = input.value;
      }
    });

    return data;
  }

  setData(formElement, data) {
    Object.entries(data).forEach(([name, value]) => {
      const input = formElement.querySelector(`[name="${name}"]`);
      if (input) {
        if (input.type === 'checkbox') {
          input.checked = value;
        } else {
          input.value = value;
        }
      }
    });
  }
}

// Usage examples
const query = new QueryBuilder()
  .select(['id', 'name', 'email'])
  .from('users')
  .leftJoin('profiles', 'users.id = profiles.user_id')
  .where('users.active = 1')
  .orWhere('users.premium = 1')
  .orderBy('users.created_at', 'DESC')
  .limit(10)
  .build();

console.log(query);
// SELECT id, name, email FROM users LEFT JOIN profiles ON users.id = profiles.user_id WHERE users.active = 1 OR users.premium = 1 ORDER BY users.created_at DESC LIMIT 10

const loginForm = new FormBuilder()
  .addEmailField('email', 'Email Address', { required: true })
  .addPasswordField('password', 'Password', { required: true })
  .addCheckboxField('remember', 'Remember me')
  .setLayout('vertical')
  .setTheme('material')
  .build();

document.body.appendChild(loginForm.element);

Behavioral Patterns

Behavioral patterns focus on communication between objects and the assignment of responsibilities.

Observer Pattern

The Observer pattern defines a one-to-many dependency between objects, so when one object changes state, all dependents are notified automatically.

// Event Emitter Observer Pattern
class EventEmitter {
  constructor() {
    this.events = new Map();
    this.maxListeners = 10;
  }

  on(event, listener) {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }

    const listeners = this.events.get(event);

    if (listeners.length >= this.maxListeners) {
      console.warn(
        `Warning: Possible EventEmitter memory leak detected. ${listeners.length + 1} ${event} listeners added.`
      );
    }

    listeners.push(listener);

    return this;
  }

  once(event, listener) {
    const onceWrapper = (...args) => {
      listener.apply(this, args);
      this.off(event, onceWrapper);
    };

    return this.on(event, onceWrapper);
  }

  off(event, listener) {
    if (!this.events.has(event)) {
      return this;
    }

    const listeners = this.events.get(event);
    const index = listeners.indexOf(listener);

    if (index > -1) {
      listeners.splice(index, 1);
    }

    if (listeners.length === 0) {
      this.events.delete(event);
    }

    return this;
  }

  emit(event, ...args) {
    if (!this.events.has(event)) {
      return false;
    }

    const listeners = this.events.get(event).slice(); // Clone to avoid issues with modifications during iteration

    listeners.forEach((listener) => {
      try {
        listener.apply(this, args);
      } catch (error) {
        console.error(`Error in event listener for ${event}:`, error);
      }
    });

    return true;
  }

  removeAllListeners(event) {
    if (event) {
      this.events.delete(event);
    } else {
      this.events.clear();
    }

    return this;
  }

  listenerCount(event) {
    return this.events.has(event) ? this.events.get(event).length : 0;
  }

  eventNames() {
    return Array.from(this.events.keys());
  }

  setMaxListeners(n) {
    this.maxListeners = n;
    return this;
  }
}

// Subject-Observer Pattern
class Subject {
  constructor() {
    this.observers = [];
    this.state = {};
  }

  subscribe(observer) {
    if (typeof observer.update !== 'function') {
      throw new Error('Observer must have an update method');
    }

    this.observers.push(observer);

    return {
      unsubscribe: () => {
        const index = this.observers.indexOf(observer);
        if (index > -1) {
          this.observers.splice(index, 1);
        }
      },
    };
  }

  unsubscribe(observer) {
    const index = this.observers.indexOf(observer);
    if (index > -1) {
      this.observers.splice(index, 1);
    }
  }

  notify(data) {
    this.observers.forEach((observer) => {
      try {
        observer.update(data, this);
      } catch (error) {
        console.error('Error notifying observer:', error);
      }
    });
  }

  setState(newState) {
    const previousState = { ...this.state };
    this.state = { ...this.state, ...newState };

    this.notify({
      type: 'stateChange',
      previousState,
      currentState: this.state,
      changes: newState,
    });
  }

  getState() {
    return { ...this.state };
  }
}

// Store with Observer Pattern
class Store extends EventEmitter {
  constructor(initialState = {}, reducer = (state, action) => state) {
    super();
    this.state = initialState;
    this.reducer = reducer;
    this.middleware = [];
  }

  getState() {
    return { ...this.state };
  }

  dispatch(action) {
    const previousState = this.state;

    // Apply middleware
    let processedAction = action;

    for (const middleware of this.middleware) {
      processedAction = middleware(this)(processedAction);
    }

    // Apply reducer
    this.state = this.reducer(this.state, processedAction);

    // Notify subscribers
    this.emit('stateChange', {
      action: processedAction,
      previousState,
      currentState: this.state,
    });

    return processedAction;
  }

  subscribe(listener) {
    return this.on('stateChange', listener);
  }

  addMiddleware(middleware) {
    this.middleware.push(middleware);
  }
}

// Model-View Observer Pattern
class Model extends EventEmitter {
  constructor(data = {}) {
    super();
    this.data = { ...data };
    this.validators = new Map();
    this.errors = new Map();
  }

  get(key) {
    return this.data[key];
  }

  set(key, value) {
    const oldValue = this.data[key];

    // Validate
    if (this.validators.has(key)) {
      const validator = this.validators.get(key);
      const isValid = validator(value);

      if (!isValid) {
        this.errors.set(key, `Invalid value for ${key}`);
        this.emit('validationError', {
          key,
          value,
          error: this.errors.get(key),
        });
        return false;
      } else {
        this.errors.delete(key);
      }
    }

    this.data[key] = value;

    this.emit('change', { key, value, oldValue });
    this.emit(`change:${key}`, { value, oldValue });

    return true;
  }

  setValidator(key, validator) {
    this.validators.set(key, validator);
  }

  isValid() {
    return this.errors.size === 0;
  }

  getErrors() {
    return new Map(this.errors);
  }

  toJSON() {
    return { ...this.data };
  }
}

class View {
  constructor(model, element) {
    this.model = model;
    this.element = element;
    this.bindings = new Map();

    this.setupEventListeners();
  }

  setupEventListeners() {
    this.model.on('change', (data) => {
      this.render(data);
    });

    this.model.on('validationError', (error) => {
      this.showError(error);
    });
  }

  render(data) {
    // Update view based on model changes
    if (this.bindings.has(data.key)) {
      const elements = this.bindings.get(data.key);
      elements.forEach((element) => {
        if (element.type === 'text') {
          element.node.textContent = data.value;
        } else if (element.type === 'value') {
          element.node.value = data.value;
        }
      });
    }
  }

  bind(key, selector, type = 'text') {
    const elements = this.element.querySelectorAll(selector);

    if (!this.bindings.has(key)) {
      this.bindings.set(key, []);
    }

    elements.forEach((element) => {
      this.bindings.get(key).push({ node: element, type });

      // Set initial value
      const value = this.model.get(key);
      if (value !== undefined) {
        if (type === 'text') {
          element.textContent = value;
        } else if (type === 'value') {
          element.value = value;
        }
      }

      // Setup two-way binding for inputs
      if (type === 'value' && element.tagName === 'INPUT') {
        element.addEventListener('input', (e) => {
          this.model.set(key, e.target.value);
        });
      }
    });
  }

  showError(error) {
    const errorElement = this.element.querySelector(
      `[data-error="${error.key}"]`
    );
    if (errorElement) {
      errorElement.textContent = error.error;
      errorElement.style.display = 'block';
    }
  }
}

// Usage examples
const eventEmitter = new EventEmitter();

eventEmitter.on('user:login', (user) => {
  console.log(`User ${user.name} logged in`);
});

eventEmitter.on('user:login', (user) => {
  console.log(`Welcome back, ${user.name}!`);
});

eventEmitter.emit('user:login', { name: 'John Doe', id: 123 });

// Store example
const counterReducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'SET_COUNT':
      return { ...state, count: action.payload };
    default:
      return state;
  }
};

const store = new Store({ count: 0 }, counterReducer);

store.subscribe((data) => {
  console.log('State changed:', data.currentState);
});

store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'SET_COUNT', payload: 10 });

// Model-View example
const userModel = new Model({ name: '', email: '', age: 0 });

userModel.setValidator('email', (value) => {
  return /\S+@\S+\.\S+/.test(value);
});

userModel.setValidator('age', (value) => {
  return value >= 0 && value <= 120;
});

// const userView = new View(userModel, document.getElementById('user-form'));
// userView.bind('name', '[data-field="name"]', 'text');
// userView.bind('email', '[data-field="email"]', 'text');

userModel.set('name', 'John Doe');
userModel.set('email', 'john@example.com');
userModel.set('age', 30);

Conclusion

Design patterns provide proven solutions to common programming problems and help create more maintainable, flexible, and understandable code. The key is knowing when and how to apply these patterns appropriately. Start with the most common patterns like Singleton, Factory, Observer, and Module patterns, then gradually incorporate others as your applications become more complex. Remember that patterns should solve actual problems in your code, not be used just for the sake of using them.