DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

JavaScript Design Patterns: Observer, Strategy, Proxy, Builder, Decorator, and Command

Design patterns are proven solutions to common programming problems. JavaScript's flexible nature lets you implement classic patterns cleanly, often more concisely than in typed languages. Here are the most useful patterns for modern JavaScript/TypeScript development.

Observer Pattern (Pub/Sub)

Components communicate without knowing about each other:

class EventEmitter {
  #listeners = new Map();

  on(event, listener) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, new Set());
    }
    this.#listeners.get(event).add(listener);
    return () => this.off(event, listener); // Returns unsubscribe function
  }

  off(event, listener) {
    this.#listeners.get(event)?.delete(listener);
  }

  emit(event, ...args) {
    this.#listeners.get(event)?.forEach(fn => fn(...args));
  }

  once(event, listener) {
    const unsubscribe = this.on(event, (...args) => {
      listener(...args);
      unsubscribe();
    });
  }
}

// Usage
const bus = new EventEmitter();

const unsubscribe = bus.on('user:login', (user) => {
  console.log('User logged in:', user.name);
});

bus.emit('user:login', { id: 1, name: 'Alice' });
unsubscribe(); // Clean up
Enter fullscreen mode Exit fullscreen mode

Strategy Pattern

Swap algorithms/behaviors at runtime:

// Different sort strategies
const strategies = {
  alphabetical: (a, b) => a.name.localeCompare(b.name),
  byDate: (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
  byScore: (a, b) => b.score - a.score,
};

class DataTable {
  #data;
  #sortStrategy;

  constructor(data, strategy = 'alphabetical') {
    this.#data = data;
    this.#sortStrategy = strategies[strategy];
  }

  setStrategy(name) {
    this.#sortStrategy = strategies[name];
    return this;
  }

  getSorted() {
    return [...this.#data].sort(this.#sortStrategy);
  }
}

const table = new DataTable(data);
table.setStrategy('byDate').getSorted();
table.setStrategy('byScore').getSorted();
Enter fullscreen mode Exit fullscreen mode

Proxy Pattern

Intercept property access, mutations, and function calls:

function createValidator(target, rules) {
  return new Proxy(target, {
    set(obj, key, value) {
      const rule = rules[key];
      if (rule && !rule.validate(value)) {
        throw new TypeError(rule.message(key, value));
      }
      obj[key] = value;
      return true;
    }
  });
}

const rules = {
  age: {
    validate: v => typeof v === 'number' && v >= 0 && v <= 150,
    message: (k, v) => `${k} must be a number between 0-150, got ${v}`,
  },
  email: {
    validate: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
    message: (k, v) => `${k} must be a valid email, got "${v}"`,
  },
};

const user = createValidator({}, rules);
user.age = 30; // OK
user.age = -5; // TypeError: age must be a number between 0-150...
user.email = 'alice@example.com'; // OK
user.email = 'notanemail'; // TypeError
Enter fullscreen mode Exit fullscreen mode

Builder Pattern

Construct complex objects step by step:

class QueryBuilder {
  #table = '';
  #conditions = [];
  #columns = ['*'];
  #limit = null;
  #order = null;

  from(table) {
    this.#table = table;
    return this; // Return this for chaining
  }

  select(...columns) {
    this.#columns = columns;
    return this;
  }

  where(condition) {
    this.#conditions.push(condition);
    return this;
  }

  orderBy(column, direction = 'ASC') {
    this.#order = `${column} ${direction}`;
    return this;
  }

  limit(n) {
    this.#limit = n;
    return this;
  }

  build() {
    let query = `SELECT ${this.#columns.join(', ')} FROM ${this.#table}`;
    if (this.#conditions.length) {
      query += ` WHERE ${this.#conditions.join(' AND ')}`;
    }
    if (this.#order) query += ` ORDER BY ${this.#order}`;
    if (this.#limit) query += ` LIMIT ${this.#limit}`;
    return query;
  }
}

const query = new QueryBuilder()
  .from('users')
  .select('id', 'name', 'email')
  .where('active = true')
  .where('age > 18')
  .orderBy('name')
  .limit(50)
  .build();

// SELECT id, name, email FROM users WHERE active = true AND age > 18 ORDER BY name ASC LIMIT 50
Enter fullscreen mode Exit fullscreen mode

Decorator Pattern

Add behavior to objects/functions without modifying them:

// Function decorator
function withLogging(fn, name = fn.name) {
  return async function(...args) {
    console.log(`${name} called with:`, args);
    const start = performance.now();
    try {
      const result = await fn.apply(this, args);
      console.log(`${name} returned:`, result, `(${(performance.now() - start).toFixed(2)}ms)`);
      return result;
    } catch (error) {
      console.error(`${name} threw:`, error);
      throw error;
    }
  };
}

function withCache(fn, ttl = 60000) {
  const cache = new Map();
  return async function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    if (cached && Date.now() - cached.timestamp < ttl) {
      return cached.value;
    }
    const value = await fn.apply(this, args);
    cache.set(key, { value, timestamp: Date.now() });
    return value;
  };
}

// Compose decorators
let fetchUser = async (id) => {
  const resp = await fetch(`/api/users/${id}`);
  return resp.json();
};

fetchUser = withLogging(fetchUser);
fetchUser = withCache(fetchUser, 30000); // Cache for 30s
Enter fullscreen mode Exit fullscreen mode

Singleton Pattern

Ensure only one instance exists:

class DatabaseConnection {
  static #instance = null;

  constructor(connectionString) {
    if (DatabaseConnection.#instance) {
      return DatabaseConnection.#instance;
    }
    this.connectionString = connectionString;
    this.connected = false;
    DatabaseConnection.#instance = this;
  }

  static getInstance() {
    if (!DatabaseConnection.#instance) {
      throw new Error('Database not initialized. Call new DatabaseConnection(url) first.');
    }
    return DatabaseConnection.#instance;
  }

  static reset() { // For testing
    DatabaseConnection.#instance = null;
  }
}

// ES Module singleton (simpler): just export an instance
// db.js
export const db = new DatabaseConnection(process.env.DB_URL);
Enter fullscreen mode Exit fullscreen mode

Command Pattern

Encapsulate operations as objects — enables undo/redo:

class CommandHistory {
  #history = [];
  #undoStack = [];

  execute(command) {
    command.execute();
    this.#history.push(command);
    this.#undoStack = []; // Clear redo stack on new command
  }

  undo() {
    const command = this.#history.pop();
    if (command) {
      command.undo();
      this.#undoStack.push(command);
    }
  }

  redo() {
    const command = this.#undoStack.pop();
    if (command) {
      command.execute();
      this.#history.push(command);
    }
  }
}

// A specific command
class AddTextCommand {
  constructor(editor, text, position) {
    this.editor = editor;
    this.text = text;
    this.position = position;
  }

  execute() {
    this.editor.insertAt(this.text, this.position);
  }

  undo() {
    this.editor.deleteAt(this.position, this.text.length);
  }
}
Enter fullscreen mode Exit fullscreen mode

These patterns aren't just academic — they appear in React (state management as observers), React Query (decorator for fetch), form libraries (builder for field configurations), and text editors (command pattern for undo/redo).

Top comments (0)