javascript

examples

examples.js
/**
 * 21.4 Design Patterns - Examples
 *
 * Demonstrating common JavaScript design patterns
 */

// ================================================
// 1. SINGLETON PATTERN
// ================================================

/**
 * Singleton - ensures only one instance exists
 */

// ES6 Class-based Singleton
class DatabaseConnection {
  constructor(config) {
    if (DatabaseConnection.instance) {
      return DatabaseConnection.instance;
    }

    this.config = config;
    this.connected = false;

    DatabaseConnection.instance = this;
  }

  connect() {
    if (!this.connected) {
      console.log('Connecting to database...');
      this.connected = true;
    }
    return this;
  }

  query(sql) {
    if (!this.connected) {
      throw new Error('Not connected');
    }
    console.log(`Executing: ${sql}`);
    return { rows: [] };
  }

  static getInstance(config) {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection(config);
    }
    return DatabaseConnection.instance;
  }
}

// Module-based Singleton (recommended for JavaScript)
const Logger = (() => {
  let instance;

  function createLogger() {
    const logs = [];

    return {
      log(message, level = 'info') {
        const entry = {
          timestamp: new Date().toISOString(),
          level,
          message,
        };
        logs.push(entry);
        console.log(`[${entry.timestamp}] ${level.toUpperCase()}: ${message}`);
      },

      getLogs() {
        return [...logs];
      },

      clear() {
        logs.length = 0;
      },
    };
  }

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

console.log('='.repeat(50));
console.log('Singleton Pattern:');
console.log('-'.repeat(50));

const db1 = new DatabaseConnection({ host: 'localhost' });
const db2 = new DatabaseConnection({ host: 'remote' }); // Returns same instance!
console.log('Same instance?', db1 === db2);

const logger = Logger.getInstance();
logger.log('Application started');

// ================================================
// 2. FACTORY PATTERN
// ================================================

/**
 * Factory - creates objects without exposing creation logic
 */

// Simple Factory
class Vehicle {
  constructor(type) {
    this.type = type;
  }

  drive() {
    return `Driving a ${this.type}`;
  }
}

class Car extends Vehicle {
  constructor() {
    super('car');
    this.wheels = 4;
  }
}

class Motorcycle extends Vehicle {
  constructor() {
    super('motorcycle');
    this.wheels = 2;
  }
}

class Truck extends Vehicle {
  constructor() {
    super('truck');
    this.wheels = 18;
  }
}

class VehicleFactory {
  static create(type) {
    switch (type.toLowerCase()) {
      case 'car':
        return new Car();
      case 'motorcycle':
        return new Motorcycle();
      case 'truck':
        return new Truck();
      default:
        throw new Error(`Unknown vehicle type: ${type}`);
    }
  }
}

// Abstract Factory
class UIFactory {
  createButton() {
    throw new Error('Must implement');
  }
  createInput() {
    throw new Error('Must implement');
  }
  createModal() {
    throw new Error('Must implement');
  }
}

class MaterialUIFactory extends UIFactory {
  createButton(text) {
    return { type: 'MaterialButton', text, style: 'material' };
  }

  createInput(placeholder) {
    return { type: 'MaterialInput', placeholder, style: 'material' };
  }

  createModal(title) {
    return { type: 'MaterialModal', title, style: 'material' };
  }
}

class BootstrapUIFactory extends UIFactory {
  createButton(text) {
    return { type: 'BootstrapButton', text, style: 'bootstrap' };
  }

  createInput(placeholder) {
    return { type: 'BootstrapInput', placeholder, style: 'bootstrap' };
  }

  createModal(title) {
    return { type: 'BootstrapModal', title, style: 'bootstrap' };
  }
}

console.log('\n' + '='.repeat(50));
console.log('Factory Pattern:');
console.log('-'.repeat(50));

const car = VehicleFactory.create('car');
const motorcycle = VehicleFactory.create('motorcycle');
console.log(car.drive(), '- Wheels:', car.wheels);
console.log(motorcycle.drive(), '- Wheels:', motorcycle.wheels);

const uiFactory = new MaterialUIFactory();
console.log('Material Button:', uiFactory.createButton('Click Me'));

// ================================================
// 3. BUILDER PATTERN
// ================================================

/**
 * Builder - constructs complex objects step by step
 */

class QueryBuilder {
  constructor() {
    this.query = {
      select: [],
      from: null,
      where: [],
      orderBy: [],
      limit: null,
      offset: null,
    };
  }

  select(...columns) {
    this.query.select = columns.length > 0 ? columns : ['*'];
    return this;
  }

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

  where(condition, value) {
    this.query.where.push({ condition, value });
    return this;
  }

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

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

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

  build() {
    let sql = `SELECT ${this.query.select.join(', ')}`;
    sql += ` FROM ${this.query.from}`;

    if (this.query.where.length > 0) {
      const conditions = this.query.where.map((w) => w.condition).join(' AND ');
      sql += ` WHERE ${conditions}`;
    }

    if (this.query.orderBy.length > 0) {
      const orders = this.query.orderBy
        .map((o) => `${o.column} ${o.direction}`)
        .join(', ');
      sql += ` ORDER BY ${orders}`;
    }

    if (this.query.limit) {
      sql += ` LIMIT ${this.query.limit}`;
    }

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

    return sql;
  }
}

// HttpRequest Builder
class HttpRequestBuilder {
  constructor() {
    this.request = {
      method: 'GET',
      url: '',
      headers: {},
      body: null,
      timeout: 5000,
    };
  }

  get(url) {
    this.request.method = 'GET';
    this.request.url = url;
    return this;
  }

  post(url) {
    this.request.method = 'POST';
    this.request.url = url;
    return this;
  }

  header(key, value) {
    this.request.headers[key] = value;
    return this;
  }

  json(data) {
    this.request.body = JSON.stringify(data);
    this.request.headers['Content-Type'] = 'application/json';
    return this;
  }

  timeout(ms) {
    this.request.timeout = ms;
    return this;
  }

  build() {
    return { ...this.request };
  }
}

console.log('\n' + '='.repeat(50));
console.log('Builder Pattern:');
console.log('-'.repeat(50));

const query = new QueryBuilder()
  .select('id', 'name', 'email')
  .from('users')
  .where('status = ?', 'active')
  .where('age > ?', 18)
  .orderBy('name', 'ASC')
  .limit(10)
  .offset(20)
  .build();

console.log('Built query:', query);

const request = new HttpRequestBuilder()
  .post('https://api.example.com/users')
  .header('Authorization', 'Bearer token')
  .json({ name: 'John', email: 'john@example.com' })
  .timeout(10000)
  .build();

console.log('Built request:', request);

// ================================================
// 4. OBSERVER PATTERN
// ================================================

/**
 * Observer - one-to-many dependency between objects
 */

class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  on(event, callback) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }
    this.events.get(event).add(callback);

    // Return unsubscribe function
    return () => this.off(event, callback);
  }

  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    return this.on(event, wrapper);
  }

  off(event, callback) {
    if (this.events.has(event)) {
      this.events.get(event).delete(callback);
    }
  }

  emit(event, ...args) {
    if (this.events.has(event)) {
      for (const callback of this.events.get(event)) {
        callback(...args);
      }
    }
  }

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

// Observable Store (React-like state management)
class ObservableStore {
  constructor(initialState = {}) {
    this.state = initialState;
    this.emitter = new EventEmitter();
  }

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

  setState(updates) {
    const prevState = this.state;
    this.state = { ...this.state, ...updates };
    this.emitter.emit('change', this.state, prevState);
  }

  subscribe(callback) {
    return this.emitter.on('change', callback);
  }
}

console.log('\n' + '='.repeat(50));
console.log('Observer Pattern:');
console.log('-'.repeat(50));

const emitter = new EventEmitter();

const unsubscribe = emitter.on('userLogin', (user) => {
  console.log(`User logged in: ${user.name}`);
});

emitter.on('userLogin', (user) => {
  console.log(`Sending welcome email to ${user.email}`);
});

emitter.emit('userLogin', { name: 'John', email: 'john@example.com' });

// Observable store example
const store = new ObservableStore({ count: 0 });
store.subscribe((newState, oldState) => {
  console.log(`Count changed: ${oldState.count} → ${newState.count}`);
});
store.setState({ count: 1 });

// ================================================
// 5. STRATEGY PATTERN
// ================================================

/**
 * Strategy - interchangeable algorithms
 */

// Payment strategies
const paymentStrategies = {
  creditCard: {
    name: 'Credit Card',
    process(amount, details) {
      console.log(
        `Processing $${amount} via Credit Card: ${details.cardNumber}`
      );
      return { success: true, transactionId: 'CC-' + Date.now() };
    },
  },

  paypal: {
    name: 'PayPal',
    process(amount, details) {
      console.log(`Processing $${amount} via PayPal: ${details.email}`);
      return { success: true, transactionId: 'PP-' + Date.now() };
    },
  },

  crypto: {
    name: 'Cryptocurrency',
    process(amount, details) {
      console.log(`Processing $${amount} via Crypto: ${details.walletAddress}`);
      return { success: true, transactionId: 'CR-' + Date.now() };
    },
  },
};

class PaymentProcessor {
  constructor(strategy = null) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  processPayment(amount, details) {
    if (!this.strategy) {
      throw new Error('Payment strategy not set');
    }
    return this.strategy.process(amount, details);
  }
}

// Sorting strategies
const sortStrategies = {
  byName: (a, b) => a.name.localeCompare(b.name),
  byPrice: (a, b) => a.price - b.price,
  byRating: (a, b) => b.rating - a.rating,
  byDate: (a, b) => new Date(b.date) - new Date(a.date),
};

class ProductList {
  constructor(products = []) {
    this.products = products;
    this.sortStrategy = sortStrategies.byName;
  }

  setSortStrategy(strategy) {
    this.sortStrategy = strategy;
  }

  sort() {
    return [...this.products].sort(this.sortStrategy);
  }
}

console.log('\n' + '='.repeat(50));
console.log('Strategy Pattern:');
console.log('-'.repeat(50));

const processor = new PaymentProcessor();

processor.setStrategy(paymentStrategies.creditCard);
console.log(processor.processPayment(100, { cardNumber: '****1234' }));

processor.setStrategy(paymentStrategies.paypal);
console.log(processor.processPayment(50, { email: 'user@paypal.com' }));

// ================================================
// 6. DECORATOR PATTERN
// ================================================

/**
 * Decorator - adds behavior dynamically
 */

// Function decorators
function withLogging(fn) {
  return function (...args) {
    console.log(`Calling ${fn.name} with:`, args);
    const result = fn.apply(this, args);
    console.log(`${fn.name} returned:`, result);
    return result;
  };
}

function withTiming(fn) {
  return function (...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    const end = performance.now();
    console.log(`${fn.name} took ${(end - start).toFixed(2)}ms`);
    return result;
  };
}

function withMemoization(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log('Cache hit for:', args);
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Class decorators (composition)
class Coffee {
  constructor() {
    this.description = 'Basic Coffee';
    this.cost = 2.0;
  }

  getDescription() {
    return this.description;
  }

  getCost() {
    return this.cost;
  }
}

class CoffeeDecorator {
  constructor(coffee) {
    this.coffee = coffee;
  }

  getDescription() {
    return this.coffee.getDescription();
  }

  getCost() {
    return this.coffee.getCost();
  }
}

class MilkDecorator extends CoffeeDecorator {
  getDescription() {
    return super.getDescription() + ', Milk';
  }

  getCost() {
    return super.getCost() + 0.5;
  }
}

class SugarDecorator extends CoffeeDecorator {
  getDescription() {
    return super.getDescription() + ', Sugar';
  }

  getCost() {
    return super.getCost() + 0.25;
  }
}

class WhipCreamDecorator extends CoffeeDecorator {
  getDescription() {
    return super.getDescription() + ', Whip Cream';
  }

  getCost() {
    return super.getCost() + 0.75;
  }
}

console.log('\n' + '='.repeat(50));
console.log('Decorator Pattern:');
console.log('-'.repeat(50));

// Function decorator example
function add(a, b) {
  return a + b;
}

const decoratedAdd = withLogging(withTiming(add));
decoratedAdd(5, 3);

// Class decorator example
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhipCreamDecorator(coffee);

console.log(`Order: ${coffee.getDescription()}`);
console.log(`Total: $${coffee.getCost().toFixed(2)}`);

// ================================================
// 7. PROXY PATTERN
// ================================================

/**
 * Proxy - controls access to objects
 */

// Validation Proxy
function createValidatedObject(target, validators) {
  return new Proxy(target, {
    set(obj, prop, value) {
      if (validators[prop]) {
        const isValid = validators[prop](value);
        if (!isValid) {
          throw new Error(`Invalid value for ${prop}: ${value}`);
        }
      }
      obj[prop] = value;
      return true;
    },
  });
}

// Logging Proxy
function createLoggingProxy(target, name) {
  return new Proxy(target, {
    get(obj, prop) {
      console.log(`[${name}] Getting ${prop}`);
      const value = obj[prop];
      return typeof value === 'function' ? value.bind(obj) : value;
    },

    set(obj, prop, value) {
      console.log(`[${name}] Setting ${prop} = ${value}`);
      obj[prop] = value;
      return true;
    },
  });
}

// Lazy Loading Proxy
function createLazyLoader(loader) {
  let value = null;
  let loaded = false;

  return new Proxy(
    {},
    {
      get(target, prop) {
        if (!loaded) {
          value = loader();
          loaded = true;
        }
        return value[prop];
      },
    }
  );
}

// Access Control Proxy
function createAccessControlProxy(target, permissions) {
  return new Proxy(target, {
    get(obj, prop) {
      if (permissions.canRead && permissions.canRead(prop)) {
        return obj[prop];
      }
      throw new Error(`Access denied: Cannot read ${prop}`);
    },

    set(obj, prop, value) {
      if (permissions.canWrite && permissions.canWrite(prop)) {
        obj[prop] = value;
        return true;
      }
      throw new Error(`Access denied: Cannot write ${prop}`);
    },
  });
}

console.log('\n' + '='.repeat(50));
console.log('Proxy Pattern:');
console.log('-'.repeat(50));

// Validation proxy example
const user = createValidatedObject(
  {},
  {
    age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
    email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
  }
);

user.age = 25;
console.log('Valid age set:', user.age);

try {
  user.age = -5;
} catch (e) {
  console.log('Caught validation error:', e.message);
}

// ================================================
// 8. COMMAND PATTERN
// ================================================

/**
 * Command - encapsulates actions as objects
 */

class Command {
  execute() {
    throw new Error('Must implement execute');
  }
  undo() {
    throw new Error('Must implement undo');
  }
}

// Text Editor Commands
class InsertTextCommand extends Command {
  constructor(editor, text, position) {
    super();
    this.editor = editor;
    this.text = text;
    this.position = position;
  }

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

  undo() {
    this.editor.deleteAt(this.position, this.text.length);
  }
}

class DeleteTextCommand extends Command {
  constructor(editor, position, length) {
    super();
    this.editor = editor;
    this.position = position;
    this.length = length;
    this.deletedText = null;
  }

  execute() {
    this.deletedText = this.editor.getTextAt(this.position, this.length);
    this.editor.deleteAt(this.position, this.length);
  }

  undo() {
    this.editor.insertAt(this.position, this.deletedText);
  }
}

// Simple text editor
class TextEditor {
  constructor() {
    this.content = '';
  }

  insertAt(position, text) {
    this.content =
      this.content.slice(0, position) + text + this.content.slice(position);
  }

  deleteAt(position, length) {
    this.content =
      this.content.slice(0, position) + this.content.slice(position + length);
  }

  getTextAt(position, length) {
    return this.content.slice(position, position + length);
  }

  getContent() {
    return this.content;
  }
}

// Command Invoker with undo/redo
class CommandManager {
  constructor() {
    this.history = [];
    this.redoStack = [];
  }

  execute(command) {
    command.execute();
    this.history.push(command);
    this.redoStack = []; // Clear redo stack
  }

  undo() {
    if (this.history.length === 0) {
      console.log('Nothing to undo');
      return;
    }

    const command = this.history.pop();
    command.undo();
    this.redoStack.push(command);
  }

  redo() {
    if (this.redoStack.length === 0) {
      console.log('Nothing to redo');
      return;
    }

    const command = this.redoStack.pop();
    command.execute();
    this.history.push(command);
  }
}

console.log('\n' + '='.repeat(50));
console.log('Command Pattern:');
console.log('-'.repeat(50));

const editor = new TextEditor();
const commandManager = new CommandManager();

commandManager.execute(new InsertTextCommand(editor, 'Hello', 0));
console.log('After insert:', editor.getContent());

commandManager.execute(new InsertTextCommand(editor, ' World', 5));
console.log('After insert:', editor.getContent());

commandManager.undo();
console.log('After undo:', editor.getContent());

commandManager.redo();
console.log('After redo:', editor.getContent());

// ================================================
// 9. MIDDLEWARE PATTERN
// ================================================

/**
 * Middleware - chain of processors
 */

class MiddlewareManager {
  constructor() {
    this.middlewares = [];
  }

  use(fn) {
    this.middlewares.push(fn);
    return this;
  }

  async execute(context) {
    let index = 0;

    const next = async () => {
      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        await middleware(context, next);
      }
    };

    await next();
    return context;
  }
}

// Example middlewares
const authMiddleware = async (ctx, next) => {
  console.log('Auth: Checking authentication...');
  if (ctx.headers?.authorization) {
    ctx.user = { id: 1, name: 'John' };
  }
  await next();
};

const loggingMiddleware = async (ctx, next) => {
  const start = Date.now();
  console.log(`Logger: ${ctx.method} ${ctx.path} started`);
  await next();
  console.log(`Logger: Completed in ${Date.now() - start}ms`);
};

const validationMiddleware = async (ctx, next) => {
  console.log('Validation: Checking request...');
  if (!ctx.body?.name) {
    ctx.error = 'Name is required';
    return; // Don't continue
  }
  await next();
};

console.log('\n' + '='.repeat(50));
console.log('Middleware Pattern:');
console.log('-'.repeat(50));

const pipeline = new MiddlewareManager();
pipeline
  .use(loggingMiddleware)
  .use(authMiddleware)
  .use(validationMiddleware)
  .use(async (ctx, next) => {
    console.log('Handler: Processing request');
    ctx.result = { success: true, user: ctx.user };
    await next();
  });

(async () => {
  const context = {
    method: 'POST',
    path: '/api/users',
    headers: { authorization: 'Bearer token' },
    body: { name: 'John' },
  };

  await pipeline.execute(context);
  console.log('Result:', context.result);
})();

// ================================================
// 10. PUBLISH/SUBSCRIBE PATTERN
// ================================================

/**
 * Pub/Sub - loose coupling between publishers and subscribers
 */

class PubSub {
  constructor() {
    this.topics = new Map();
  }

  subscribe(topic, callback) {
    if (!this.topics.has(topic)) {
      this.topics.set(topic, new Set());
    }

    this.topics.get(topic).add(callback);

    // Return unsubscribe function
    return () => {
      this.topics.get(topic).delete(callback);
    };
  }

  publish(topic, data) {
    if (!this.topics.has(topic)) {
      return;
    }

    for (const callback of this.topics.get(topic)) {
      // Async to prevent blocking
      setTimeout(() => callback(data), 0);
    }
  }

  // Subscribe to pattern (simple wildcard support)
  subscribePattern(pattern, callback) {
    const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');

    return this.subscribe('__all__', (topic, data) => {
      if (regex.test(topic)) {
        callback(data, topic);
      }
    });
  }

  publishWithPattern(topic, data) {
    this.publish(topic, data);
    this.publish('__all__', { topic, data });
  }
}

console.log('\n' + '='.repeat(50));
console.log('Pub/Sub Pattern:');
console.log('-'.repeat(50));

const pubsub = new PubSub();

// Subscribe to specific topics
pubsub.subscribe('user:created', (data) => {
  console.log('Email service: Sending welcome email to', data.email);
});

pubsub.subscribe('user:created', (data) => {
  console.log('Analytics: Tracking new user', data.id);
});

pubsub.subscribe('order:placed', (data) => {
  console.log('Inventory: Updating stock for order', data.orderId);
});

// Publish events
pubsub.publish('user:created', { id: 1, email: 'john@example.com' });
pubsub.publish('order:placed', { orderId: 'ORD-123', items: 3 });

// ================================================
// EXPORTS
// ================================================

module.exports = {
  // Singletons
  DatabaseConnection,
  Logger,

  // Factories
  VehicleFactory,
  MaterialUIFactory,
  BootstrapUIFactory,

  // Builders
  QueryBuilder,
  HttpRequestBuilder,

  // Observer
  EventEmitter,
  ObservableStore,

  // Strategy
  paymentStrategies,
  PaymentProcessor,
  sortStrategies,
  ProductList,

  // Decorators
  withLogging,
  withTiming,
  withMemoization,
  Coffee,
  MilkDecorator,
  SugarDecorator,
  WhipCreamDecorator,

  // Proxy
  createValidatedObject,
  createLoggingProxy,
  createLazyLoader,
  createAccessControlProxy,

  // Command
  Command,
  InsertTextCommand,
  DeleteTextCommand,
  TextEditor,
  CommandManager,

  // Middleware
  MiddlewareManager,

  // Pub/Sub
  PubSub,
};
Examples - JavaScript Tutorial | DeepML